3 out of 4 emails could be categorized automatically with 90% precision.
How many of your customer emails get forwarded at least once before they land with the right person?
If an agent spends 30 seconds reading and sorting each email, and you get 500 a day, that's over 4 hours a day. Every single day. Time that comes straight out of what customer service is actually there for: helping customers.
We're on a mission to remove the things that make customer service heavy. Emails sitting in the wrong queue. Customers waiting because nobody saw that it was urgent. Agents spending their day sorting instead of solving problems.
At the same time, a big part of the solution is often already sitting in your own systems. Every time an agent has moved an email to the right queue, set a priority, or marked a case as an incident, they've effectively created a training example. Most customer service teams are sitting on thousands of them without ever using them.
We wanted to find out how far that could take us.
Why not just use a large language model?
That's the first question most people ask, and it's a good one. Large language models like the ones behind ChatGPT are impressive at writing, summarizing and reasoning. But sorting thousands of emails a day is a different kind of task.
It's closer to what an experienced agent does in two seconds: glances at the email and instantly knows it belongs to finance, and that it's urgent. That doesn't call for a long piece of text. It calls for a fast, reliable decision, and a clear sense of when you're not sure.
A language model is built to write, not to decide. It has to turn its answer into text, which then has to be interpreted. It's slower and more expensive per email. It can phrase a wrong answer just as convincingly as a right one. And its own sense of how confident it is doesn't necessarily match how often it's actually right.
That's why we don't think the large language model is the final answer for customer service. It's one of the answers. It's good at drafting replies and helping with complicated cases. But for the many fast, repetitive decisions that currently eat into agents' time, a different tool is needed.

Fast decisions and slow thinking are two different tasks. It makes sense to use different tools for them.
What we did
We trained our own AI model for the purpose, a so-called System One model. The name comes from psychology, where "system 1" is the fast, intuitive part of our thinking. The model doesn't write text, it makes fast decisions. It reads an email and answers three questions at once:
- Which department should get the email?
- How urgent is it?
- What kind of request is it: an incident, a request, a problem, or a change?

The model doesn't write a reply. It makes three decisions about the email and tells you how confident it is in each of them.
The model learned from 26,000 support emails in German and English, and we tested it on 1,225 German emails it had never seen. To know whether the results were actually good, we measured it against the alternatives a customer service team would realistically consider: guessing the most common answer, a classic statistical method, a hosted commercial decision model, and an open decision model built on a large language model. All of them were tested on exactly the same emails, so the comparison is fair.
The results
The three questions turned out to be very different in difficulty:
| Question | Options | Correct answer | Handled automatically at 90% precision |
|---|---|---|---|
| What kind of request | 4 | 83% | 77% |
| How urgent is it | 3 | 56% | 3% |
| Which department | 10 | 46% | 10% |
The last column is the important one. It shows how large a share of the emails the model was confident enough to handle itself, and among those, it was right 9 times out of 10.
For the request-type question, the model could handle 77.2% of emails automatically at 90% precision. The rest, the model forwarded to a human.
That's an important distinction. We're not trying to build a model that has to answer everything. We'd rather have a model that can recognize the emails it's confident enough about, and let a human take the rest.
The department and urgency choices were far harder, and here it's worth being blunt: none of the methods we tested did well on them. Not the classic method, not the large commercial models, and not our own either. When very different methods all hit the same ceiling, the problem is in the data and the categories, not the methods. We'll come back to why.
On the department choice, our model could still handle 10% of emails automatically at the same precision requirement. That's not a lot, but it's more than double the classic method.
One of the more surprising results was that the large, general decision models without training actually did worse on the department choice than simply picking the most common department. Size alone doesn't do it. When the task is about a company's own categories and decisions, it's hard to beat training on the right data.

Share of emails that can be handled automatically at 90% precision. The large, general model without training couldn't automate a single department choice safely.
Three things we didn't expect
1. Over half the data had to be thrown out
The dataset looked clean at first glance. But once we dug into it, it turned out that over half of the German emails came from a different version with different categories, a different urgency scale, and missing information.
Had we trained on all of it, the model would have learned to blend two different sorting systems together.

Over half the German emails had to be excluded because they followed a different sorting system.
The same thing happens in real ticket systems. Categories get renamed, departments get merged, and priority scales get changed over the years. The quality and consistency of your historical sorting therefore sets a ceiling on what a model can learn from it.
It might not be as exciting as picking a bigger model, but it's probably one of the most important things to get right first.
2. A classic method from the 1970s was harder to beat than we thought
We compared against a simple statistical method that sorts by which words appear in the email. It trains in a few seconds, needs very little computing power, and on overall precision it was almost as good as our model.
The difference showed up somewhere else.
Our model was significantly better at the rare departments, the ones that only get a handful of emails, and which the simple method typically overlooks. Overall precision was 44.2% for the classic method versus 45.5% for our model, essentially no difference.
But when we measure with macro-F1, which gives the rare departments the same weight as the large ones, we get 0.26 versus 0.37.

Almost the same overall precision, but our model does markedly better on the rare departments.
The lesson is that you have to measure against the simple alternative. If an advanced model can't beat a method that trains in a few seconds, there isn't much reason to use it. But you also need to pick a metric that reflects the problem you're actually trying to solve.
3. What matters most isn't how often the model is right, but whether it knows when it's right
Since overall precision was nearly a tie, why could our model automate more than twice as many department choices safely?
Because its own confidence can be used to control when it's allowed to act.
When the model is very confident in an answer, we can let it handle the email automatically. When it's unsure, we forward it to a human. That way, the model doesn't have to be perfect to be useful.
This is also where the 90% comes in. You decide how confident the model has to be before it's allowed to act on its own. Set the bar high, and you automate less, but with higher certainty. Set it lower, and more emails can be handled automatically.

You set the threshold. Everything the model is confident enough about is handled automatically; the rest goes to an agent.
That means automation doesn't have to be about replacing agents. It can just as easily be about removing the part of the work where an agent spends time reading an email, finding the right queue, and clicking the right category.
Why the department choice is hard
When even the best method only gets under half of the department choices right, that's not just about the model.
Many of the categories overlap: Technical Support, Product Support, IT Support and Customer Service often describe the same emails. An email about a printer that isn't working could reasonably belong to three of them.

Most errors happen between four departments that describe the same kinds of emails. The problem is in the categories, not just the model.
That's an insight worth taking home: before you automate routing, the categories need to be sharp enough that two agents would sort the same email roughly the same way.
If humans themselves disagree about which department a case belongs to, it's hard to expect a model to solve that problem on its own.
On top of that, the ground truth isn't always right. The dataset we tested on is generated, not sorted by real agents, and we found examples where the department in the data itself was wrong. An email about Office programs failing to launch, for instance, was filed under HR. No model can score well against a ground truth that's itself wrong.
That's why we always start by looking at the categories before training anything. That's often where the biggest gain is.
A question you should ask any AI vendor
Along the way, we also discovered something that has little to do with the model itself: training data.
Many of the popular public datasets that AI models are trained on carry licenses or terms of use that restrict commercial use. That can be fine for research and experiments, but it suddenly becomes relevant once the model is meant to be used in a commercial product.
We've therefore documented the source and license of every single dataset the models we build for customers are trained on, and excluded data where the terms were unclear.
So when you're looking at AI solutions for your customer service, ask:
What was the model trained on, and is it allowed to be used commercially?
A good answer is specific. A vague answer is worth looking into further.
What it takes
The method is the same on your own data. You need historical cases with queue, category or priority, and preferably enough examples that the different types of requests are actually represented.
The categories don't need to be perfect. But if their meaning has changed several times over the years, that needs to be handled before you start training.
And then there needs to be a decision about where your data gets processed. The model can run on your own servers or in our cloud in Denmark. Whichever you choose, your data never leaves the country, and you're not sending it to a foreign AI service.
The results here are from a public dataset. On your own emails they'll be different, and that's exactly why it's worth measuring.
What can your own data do?
Our mission isn't to put a chatbot in front of your customers. It's to remove the friction behind the scenes, so your agents can spend their time on the conversations that actually need a human.
We're looking for 2-3 customer service teams who want to help find out how far this can go.
We take your historical cases, train and test a model on them, and show you how large a share of your emails can be handled safely and automatically. We also show you which categories work well, where the model becomes uncertain, and where your existing sorting might be too unclear to automate.
You don't have to take our word for it that AI can make a difference.
We can measure it on your own data.
Get in touch if that's you.
Try the model yourself
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Sans:wght@400;500;600&family=IBM+Plex+Mono:wght@400;500&display=swap" rel="stylesheet">
<style>
* { box-sizing: border-box; }
html, body { margin: 0; padding: 0; }
body { font-family: 'IBM Plex Sans', system-ui, sans-serif; background: transparent; }
textarea::placeholder { color: #64748B; }
.lm-page { padding: 24px; }
.lm-card { max-width: 880px; margin: 0 auto; border-radius: 20px; border: 1px solid rgba(59,130,246,0.28); background: linear-gradient(135deg, #0F172A 0%, #12275A 60%, #0B3E6B 100%); padding: 32px clamp(18px, 4vw, 36px); display: flex; flex-direction: column; gap: 24px; color: #F8FAFC; }
.lm-kicker { color: #7DD3FC; font-size: 13px; font-weight: 500; letter-spacing: 0.16em; }
.lm-title { font-size: 28px; font-weight: 600; letter-spacing: -0.01em; line-height: 1.2; }
.lm-lead { color: #B9C7DC; font-size: 15px; line-height: 1.55; max-width: 620px; }
.lm-label { color: #94A3B8; font-size: 12px; letter-spacing: 0.12em; text-transform: uppercase; }
.lm-chips { display: flex; flex-wrap: nowrap; gap: 6px; align-items: center; overflow-x: auto; scrollbar-width: none; }
.lm-chips::-webkit-scrollbar { display: none; }
.lm-chip { font-family: inherit; font-size: 13px; white-space: nowrap; flex-shrink: 0; padding: 6px 10px; border-radius: 8px; cursor: pointer; background: transparent; border: 1px solid #334155; color: #CBD5E1; transition: all 150ms; display: flex; align-items: center; gap: 6px; }
.lm-chip:hover { border-color: #22D3EE; }
.lm-chip.on { background: rgba(34,211,238,0.14); border-color: #22D3EE; color: #A5F3FC; }
.lm-chip svg { stroke: #7DD3FC; flex-shrink: 0; display: block; }
.lm-chip.on svg { stroke: #22D3EE; }
.lm-inputbox { border-radius: 14px; background: rgba(11,21,38,0.7); border: 1px solid rgba(59,130,246,0.34); display: flex; flex-direction: column; overflow: hidden; }
.lm-inputbox:focus-within { border-color: #22D3EE; }
.lm-textarea { font-family: inherit; font-size: 16px; line-height: 1.55; color: #F8FAFC; background: transparent; border: none; outline: none; resize: vertical; padding: 16px 18px 8px; min-height: 96px; }
.lm-inputfooter { display: flex; justify-content: space-between; align-items: center; gap: 12px; padding: 10px 12px 12px 18px; flex-wrap: wrap; }
.lm-charcount { color: #64748B; font-size: 12px; font-family: 'IBM Plex Mono', monospace; }
.lm-runbtn { font-family: inherit; font-size: 15px; font-weight: 600; white-space: nowrap; flex-shrink: 0; padding: 10px 18px; border-radius: 10px; border: none; background: #22D3EE; color: #0F172A; cursor: pointer; display: flex; align-items: center; gap: 8px; }
.lm-runbtn:hover:not(:disabled) { background: #67E8F9; }
.lm-runbtn:disabled { cursor: default; }
.lm-deptrow { display: flex; justify-content: space-between; align-items: center; gap: 16px; flex-wrap: wrap; }
.lm-thresholdlabel { display: flex; align-items: center; gap: 10px; color: #CBD5E1; font-size: 13px; }
.lm-thresholdlabel input[type=range] { accent-color: #22D3EE; width: 140px; }
.lm-thresholdval { font-family: 'IBM Plex Mono', monospace; color: #F8FAFC; min-width: 44px; }
.lm-tiles { display: grid; grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); gap: 8px; }
.lm-tile { border-radius: 10px; padding: 10px 12px; display: flex; flex-direction: column; justify-content: space-between; gap: 4px; min-height: 58px; background: rgba(11,21,38,0.55); border: 1.5px solid rgba(59,130,246,0.22); transition: background 120ms, border-color 120ms, transform 200ms; transform: scale(1); }
.lm-tile-name { font-size: 13px; font-weight: 500; line-height: 1.25; color: #CBD5E1; }
.lm-tile-pct { font-size: 12px; font-family: 'IBM Plex Mono', monospace; color: #64748B; }
.lm-verdict { border-radius: 12px; padding: 14px 18px; display: flex; align-items: center; gap: 12px; flex-wrap: wrap; background: rgba(148,163,184,0.08); border: 1px solid rgba(148,163,184,0.35); opacity: 0.35; transform: translateY(4px); transition: opacity 350ms, transform 350ms, background 350ms, border-color 350ms; }
.lm-verdict-dot { width: 10px; height: 10px; border-radius: 50%; background: #CBD5E1; flex-shrink: 0; }
.lm-verdict-lead { flex: 1 1 240px; min-width: 0; color: #CBD5E1; font-size: 15px; line-height: 1.4; }
.lm-verdict-target { color: #F8FAFC; font-size: 17px; font-weight: 600; white-space: nowrap; }
.lm-verdict-pct { margin-left: auto; font-family: 'IBM Plex Mono', monospace; font-size: 15px; white-space: nowrap; color: #CBD5E1; }
.lm-timing { display: flex; align-items: center; gap: 10px 18px; flex-wrap: wrap; font-size: 13px; color: #94A3B8; opacity: 0.35; transition: opacity 350ms; }
.lm-timing-item { display: flex; align-items: center; gap: 7px; }
.lm-timing-val { font-family: 'IBM Plex Mono', monospace; font-size: 14px; white-space: nowrap; color: #F8FAFC; }
.lm-timing-factor { white-space: nowrap; color: #67E8F9; font-family: 'IBM Plex Mono', monospace; }
.lm-details { display: grid; grid-template-columns: repeat(auto-fit, minmax(230px, 1fr)); gap: 12px; }
.lm-card2 { border-radius: 14px; background: rgba(11,21,38,0.66); border: 1px solid rgba(59,130,246,0.28); padding: 18px; display: flex; flex-direction: column; gap: 14px; }
.lm-card2-head { display: flex; justify-content: space-between; align-items: center; gap: 8px; }
.lm-card2-title { font-size: 15px; font-weight: 600; }
.lm-badge { font-size: 11px; letter-spacing: 0.08em; text-transform: uppercase; padding: 3px 8px; border-radius: 5px; background: rgba(148,163,184,0.1); color: #64748B; }
.lm-barrow { display: flex; flex-direction: column; gap: 5px; }
.lm-barrow-head { display: flex; justify-content: space-between; gap: 8px; font-size: 13px; min-width: 0; }
.lm-barrow-name { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: #94A3B8; }
.lm-barrow-pct { flex-shrink: 0; white-space: nowrap; font-family: 'IBM Plex Mono', monospace; color: #94A3B8; }
.lm-bartrack { height: 6px; border-radius: 3px; background: rgba(148,163,184,0.18); overflow: hidden; }
.lm-barfill { height: 6px; border-radius: 3px; width: 0%; background: #475569; transition: width 700ms cubic-bezier(.2,.8,.2,1); }
.lm-urgplot { display: flex; align-items: flex-end; gap: 12px; height: 120px; padding-top: 6px; }
.lm-urgcol { flex: 1; display: flex; flex-direction: column; align-items: center; gap: 6px; height: 100%; justify-content: flex-end; }
.lm-urgpct { font-size: 13px; white-space: nowrap; font-family: 'IBM Plex Mono', monospace; color: #94A3B8; }
.lm-urgbar { width: 100%; max-width: 56px; border-radius: 6px 6px 0 0; height: 3px; background: #475569; transition: height 700ms cubic-bezier(.2,.8,.2,1), background 200ms; }
.lm-urglabels { display: flex; gap: 12px; border-top: 1px solid #334155; padding-top: 8px; }
.lm-urglabel { flex: 1; text-align: center; font-size: 13px; color: #94A3B8; }
.lm-donutwrap { display: flex; align-items: center; gap: 12px; }
.lm-donut { position: relative; width: 104px; height: 104px; flex-shrink: 0; }
.lm-donut svg { display: block; transform: rotate(-90deg); }
.lm-donut-center { position: absolute; inset: 0; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 1px; }
.lm-donut-pct { font-size: 18px; font-weight: 600; white-space: nowrap; font-family: 'IBM Plex Mono', monospace; }
.lm-donut-name { font-size: 11px; color: #93A7C4; }
.lm-legend { flex: 1; display: flex; flex-direction: column; gap: 7px; min-width: 0; }
.lm-legend-row { display: flex; align-items: center; gap: 8px; font-size: 13px; }
.lm-legend-swatch { width: 10px; height: 10px; border-radius: 3px; flex-shrink: 0; }
.lm-legend-name { color: #CBD5E1; white-space: nowrap; }
.lm-legend-pct { margin-left: auto; white-space: nowrap; font-family: 'IBM Plex Mono', monospace; color: #94A3B8; }
.lm-footer { display: flex; justify-content: space-between; align-items: center; gap: 16px; flex-wrap: wrap; border-top: 1px solid rgba(59,130,246,0.2); padding-top: 16px; }
.lm-footnote { color: #7C8DA8; font-size: 12px; line-height: 1.5; max-width: 520px; }
.lm-brand { display: flex; align-items: center; gap: 10px; }
.lm-brand img { height: 24px; width: 24px; display: block; }
.lm-brand span { color: #F8FAFC; font-size: 12px; letter-spacing: 0.1em; }
</style>
</head>
<body>
<div class="lm-page"><div class="lm-card" id="card">
<div style="display:flex;flex-direction:column;gap:8px;">
<div class="lm-kicker" id="kicker"></div>
<div class="lm-title" id="title"></div>
<div class="lm-lead" id="lead"></div>
</div>
<div style="display:flex;flex-direction:column;gap:12px;">
<div class="lm-label" id="samplesLabel"></div>
<div class="lm-chips" id="chips"></div>
<div class="lm-inputbox">
<textarea class="lm-textarea" id="textarea" rows="4"></textarea>
<div class="lm-inputfooter">
<div class="lm-charcount" id="charCount"></div>
<button class="lm-runbtn" id="runBtn" type="button"><span id="runLabel"></span> <span>→</span></button>
</div>
</div>
</div>
<div style="display:flex;flex-direction:column;gap:14px;">
<div class="lm-deptrow">
<div class="lm-label" id="deptLabel"></div>
<label class="lm-thresholdlabel">
<span id="thresholdLabel"></span>
<input type="range" min="50" max="99" value="80" id="thresholdSlider">
<span class="lm-thresholdval" id="thresholdVal"></span>
</label>
</div>
<div class="lm-tiles" id="tiles"></div>
<div class="lm-verdict" id="verdict">
<div class="lm-verdict-dot" id="verdictDot"></div>
<div class="lm-verdict-lead" id="verdictLead">— <span class="lm-verdict-target" id="verdictTarget"></span></div>
<div class="lm-verdict-pct" id="verdictPct"></div>
</div>
<div class="lm-timing" id="timing">
<div class="lm-timing-item">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="#67E8F9" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 8v5l3 2M9 2h6M12 2v3M12 22a8.5 8.5 0 1 0 0-17 8.5 8.5 0 0 0 0 17z"></path></svg>
<span id="decTimeLabel"></span><span class="lm-timing-val" id="msVal"></span>
</div>
<div class="lm-timing-item"><span id="manualLabel"></span><span class="lm-timing-val" id="manualVal" style="color:#CBD5E1;"></span></div>
<div class="lm-timing-factor" id="factorVal"></div>
</div>
</div>
<div class="lm-details">
<div class="lm-card2">
<div class="lm-card2-head"><div class="lm-card2-title" id="deptCardTitle"></div><div class="lm-badge" id="deptBadge"></div></div>
<div id="deptBars"></div>
</div>
<div class="lm-card2">
<div class="lm-card2-head"><div class="lm-card2-title" id="urgCardTitle"></div><div class="lm-badge" id="urgBadge"></div></div>
<div class="lm-urgplot" id="urgPlot"></div>
<div class="lm-urglabels" id="urgLabels"></div>
</div>
<div class="lm-card2">
<div class="lm-card2-head"><div class="lm-card2-title" id="typeCardTitle"></div><div class="lm-badge" id="typeBadge"></div></div>
<div class="lm-donutwrap">
<div class="lm-donut">
<svg width="104" height="104" viewBox="0 0 112 112">
<circle cx="56" cy="56" r="42" fill="none" stroke="rgba(148,163,184,0.15)" stroke-width="14"></circle>
<g id="donutSegs"></g>
</svg>
<div class="lm-donut-center">
<div class="lm-donut-pct" id="donutPct"></div>
<div class="lm-donut-name" id="donutName"></div>
</div>
</div>
<div class="lm-legend" id="legend"></div>
</div>
</div>
</div>
<div class="lm-footer">
<div class="lm-footnote" id="footnote"></div>
<div class="lm-brand"><img src="https://liviate.com/liviate-mark.svg" alt=""><span>infrastructure, simplified.</span></div>
</div>
</div></div>
<script>
const LANG = "en";
const DEPTS = {"da":["Teknisk support","Produktsupport","IT-support","Kundeservice","Fakturering og betaling","Returnering og ombytning","Driftsforstyrrelser","Salg og førsalg","HR","Generelle henvendelser"],"de":["Technischer Support","Produktsupport","IT-Support","Kundenservice","Abrechnung und Zahlung","Rückgabe und Umtausch","Störungen und Wartung","Vertrieb und Vorverkauf","Personalwesen","Allgemeine Anfragen"],"en":["Technical Support","Product Support","IT Support","Customer Service","Billing and Payments","Returns and Exchanges","Outages and Maintenance","Sales and Pre-Sales","Human Resources","General Inquiry"]};
const T = {"da":{"kicker":"PRØV MODELLEN","decTime":"Beslutningstid","manual":"Manuel sortering","title":"Hvor skal mailen hen?","lead":"Vælg et eksempel eller skriv din egen kundemail. Modellen træffer tre beslutninger og viser, hvor sikker den er på hver af dem.","samplesLabel":"Eksempler","placeholder":"Skriv en kundemail …","run":"Sortér mail","running":"Sorterer …","threshold":"Sikkerhedsgrænse","dept":"Afdeling","urg":"Hastegrad","type":"Type","auto":"Automatisk","human":"Medarbejder","verdictAuto":"Sendes automatisk til","verdictHuman":"For usikker. Sendes til en medarbejder. Bedste gæt:","chars":"tegn","urgL":["Lav","Mellem","Høj"],"typeL":["Fejl","Forespørgsel","Problem","Ændring"],"footnote":"Demo: svarene er illustrative og beregnes i browseren, ikke af den trænede model.","samples":[["Dobbelt betaling","Hej. Jeg kan se, at mit abonnement for marts er trukket to gange på mit kort. Kan I tilbagebetale det ene beløb hurtigst muligt? Mvh. Maria"],["Tilbud på licenser","Hej, vi er 40 medarbejdere og overvejer at købe licenser til hele teamet. Kan I sende et tilbud og evt. booke en demo i næste uge?"],["Platformen er nede","Hele platformen er nede for os siden kl. 8 i morges. Ingen af vores medarbejdere kan logge ind. Det haster!"],["Printer efter opdatering","Printeren på 2. sal virker ikke længere efter gårsdagens opdatering. Den står bare og blinker."],["Bytte størrelse","Hej, jeg vil gerne bytte de sko, jeg modtog i går, til en størrelse 42. Hvordan gør jeg det?"]]},"de":{"kicker":"MODELL TESTEN","decTime":"Entscheidungszeit","manual":"Manuelle Sortierung","title":"Wohin gehört diese Mail?","lead":"Wählen Sie ein Beispiel oder schreiben Sie Ihre eigene Kundenmail. Das Modell trifft drei Entscheidungen und zeigt, wie sicher es sich jeweils ist.","samplesLabel":"Beispiele","placeholder":"Kundenmail eingeben …","run":"Mail sortieren","running":"Sortiert …","threshold":"Sicherheitsgrenze","dept":"Abteilung","urg":"Dringlichkeit","type":"Typ","auto":"Automatisch","human":"Mitarbeitende","verdictAuto":"Wird automatisch weitergeleitet an","verdictHuman":"Zu unsicher. Geht an Mitarbeitende. Bester Tipp:","chars":"Zeichen","urgL":["Niedrig","Mittel","Hoch"],"typeL":["Störung","Anfrage","Problem","Änderung"],"footnote":"Demo: Die Antworten sind illustrativ und werden im Browser berechnet, nicht vom trainierten Modell.","samples":[["Doppelte Abbuchung","Hallo, ich sehe, dass mein Abo für März zweimal von meiner Karte abgebucht wurde. Können Sie einen der Beträge so schnell wie möglich erstatten? Viele Grüße, Maria"],["Angebot für Lizenzen","Hallo, wir sind 40 Mitarbeitende und überlegen, Lizenzen für das ganze Team zu kaufen. Können Sie uns ein Angebot schicken und nächste Woche eine Demo einplanen?"],["Plattform ausgefallen","Die gesamte Plattform ist bei uns seit 8 Uhr ausgefallen. Niemand kann sich einloggen. Es ist dringend!"],["Drucker nach Update","Der Drucker im 2. Stock funktioniert seit dem gestrigen Update nicht mehr. Er blinkt nur noch."],["Größe umtauschen","Hallo, ich möchte die Schuhe, die ich gestern bekommen habe, gegen Größe 42 umtauschen. Wie mache ich das?"]]},"en":{"kicker":"TRY THE MODEL","decTime":"Decision time","manual":"Manual sorting","title":"Where should this email go?","lead":"Pick an example or write your own customer email. The model makes three decisions and shows how confident it is in each.","samplesLabel":"Examples","placeholder":"Type a customer email …","run":"Route email","running":"Routing …","threshold":"Confidence threshold","dept":"Department","urg":"Urgency","type":"Type","auto":"Automatic","human":"Agent","verdictAuto":"Routed automatically to","verdictHuman":"Not confident enough. Sent to an agent. Best guess:","chars":"characters","urgL":["Low","Medium","High"],"typeL":["Incident","Request","Problem","Change"],"footnote":"Demo: results are illustrative and computed in the browser, not by the trained model.","samples":[["Charged twice","Hi, I can see that my subscription for March has been charged to my card twice. Could you refund one of the payments as soon as possible? Best, Maria"],["Licence quote","Hi, we are a team of 40 and are considering buying licences for everyone. Could you send us a quote and maybe book a demo next week?"],["Platform down","The whole platform has been down for us since 8 this morning. None of our staff can log in. This is urgent!"],["Printer after update","The printer on the 2nd floor stopped working after yesterday’s update. It just keeps blinking."],["Exchange a size","Hi, I’d like to exchange the shoes I received yesterday for a size 42. How do I do that?"]]}};
const FIXED = [{"dept":[0.01,0.01,0.01,0.03,0.93,0.005,0.005,0.005,0,0.005],"urg":[0.04,0.08,0.88],"type":[0.05,0.03,0.91,0.01]},{"dept":[0.01,0.02,0,0.02,0.03,0,0,0.91,0,0.01],"urg":[0.72,0.24,0.04],"type":[0.02,0.94,0.02,0.02]},{"dept":[0.02,0,0.01,0.01,0,0,0.95,0,0,0.01],"urg":[0.01,0.03,0.96],"type":[0.93,0.01,0.05,0.01]},{"dept":[0.41,0.14,0.33,0.07,0,0,0.03,0,0,0.02],"urg":[0.18,0.61,0.21],"type":[0.78,0.06,0.12,0.04]},{"dept":[0,0.02,0,0.06,0.02,0.89,0,0,0,0.01],"urg":[0.66,0.3,0.04],"type":[0.03,0.92,0.01,0.04]}];
const KW = [["fejl","error","bug","crash","login","log ind","password","adgangskode","app","software","fehler","absturz","passwort","einloggen"],["produkt","product","funktion","feature","hvordan","how do i","wie kann","indstilling","setting","einstellung"],["printer","drucker","netværk","network","netzwerk","vpn","office","outlook","laptop","pc","wifi","wlan","computer"],["kundeservice","customer service","kundenservice","klage","complaint","beschwerde","levering","delivery","lieferung","ordre","order","bestellung","pakke","package","paket"],["faktura","invoice","rechnung","betaling","payment","zahlung","trukket","charged","abgebucht","refund","tilbagebetal","erstatt","beløb","betrag","amount","kort","card","karte"],["retur","return","bytte","exchange","umtausch","rückgabe","umtauschen","størrelse","size","größe","fortryd"],["nede","down","ausfall","ausgefallen","nedbrud","outage","vedligehold","maintenance","wartung","utilgængelig","unavailable","nicht erreichbar","störung"],["tilbud","quote","angebot","licens","licence","license","lizenz","køb","buy","purchase","kaufen","demo","pris","pricing","preis"],["ferie","vacation","urlaub","løn","salary","gehalt","ansættelse","job","kontrakt","contract","vertrag","sygemeld","sick","krank"],["spørgsmål","question","frage","information","åbningstid","opening hours","öffnungszeiten","adresse","address"]];
const URG_KW = [["når i får tid","no rush","keine eile","tilbud","quote","angebot","information","spørgsmål","question"],[],["haster","akut","hurtigst","straks","asap","urgent","immediately","sofort","dringend","nede","down","ausgefallen","kritisk","critical","kritisch","!"]];
const TYPE_KW = [["virker ikke","not working","funktioniert nicht","fejl","error","fehler","nede","down","crash","ausgefallen","blinker"],["kan i","could you","können sie","ønsker","would like","möchte","tilbud","quote","angebot","bitte","gerne","please","?"],["igen","again","wieder","gentagne","repeatedly","hver gang","every time","immer noch","still"],["ændre","change","ändern","opdater","update","flytte","move","opsige","cancel","kündigen"]];
const ICONS = ["M3 7h14a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H3zM3 11h16M7 5h14v10M6 15h3","M6 3h9l4 4v14H6zM15 3v4h4M9 12h7M9 16h5","M13 2L4 14h7l-1 8 9-12h-7z","M7 9V3h10v6M6 18H4v-7a2 2 0 0 1 2-2h12a2 2 0 0 1 2 2v7h-2M7 14h10v7H7z","M4 8h13l-3-3M20 16H7l3 3"];
const TYPE_COLORS = ["#22D3EE","#3B82F6","#A5F3FC","#64748B"];
(function () {
const t = T[LANG];
const depts = DEPTS[LANG];
const en = LANG === 'en';
const pct = v => Math.round(v * 100) + (en ? '%' : ' %');
const fmtInt = n => n.toLocaleString(en ? 'en-US' : (LANG === 'de' ? 'de-DE' : 'da-DK'));
const hits = (s, list) => list.reduce((n, k) => n + (s.includes(k) ? 1 : 0), 0);
const softmax = (xs, k) => { const e = xs.map(x => Math.exp(x * k)); const z = e.reduce((a, b) => a + b, 0); return e.map(v => v / z); };
const norm = xs => { const z = xs.reduce((a, b) => a + b, 0) || 1; return xs.map(v => v / z); };
function classify(text) {
const s = ' ' + text.toLowerCase() + ' ';
return {
dept: softmax(KW.map((l, i) => (i === 9 ? 0.6 : 0.2) + 1.6 * hits(s, l)), 1.25),
urg: softmax([0.6 + 1.4 * hits(s, URG_KW[0]), 1.0, 0.4 + 1.3 * hits(s, URG_KW[2])], 1.3),
type: softmax([0.3 + 1.5 * hits(s, TYPE_KW[0]), 0.7 + 1.1 * hits(s, TYPE_KW[1]), 0.2 + 1.4 * hits(s, TYPE_KW[2]), 0.2 + 1.6 * hits(s, TYPE_KW[3])], 1.3),
};
}
// ---- one-time DOM construction (kept stable across renders so CSS transitions animate) ----
const $ = id => document.getElementById(id);
const card = $('card');
$('kicker').textContent = t.kicker;
$('title').textContent = t.title;
$('lead').textContent = t.lead;
$('samplesLabel').textContent = t.samplesLabel;
$('deptLabel').textContent = t.dept;
$('thresholdLabel').textContent = t.threshold;
$('decTimeLabel').textContent = t.decTime + ' ';
$('manualLabel').textContent = t.manual + ' ';
$('deptCardTitle').textContent = t.dept;
$('urgCardTitle').textContent = t.urg;
$('typeCardTitle').textContent = t.type;
$('footnote').textContent = t.footnote;
const textarea = $('textarea');
textarea.placeholder = t.placeholder;
const runBtn = $('runBtn');
const runLabel = $('runLabel');
const thresholdSlider = $('thresholdSlider');
// Sample chips (rebuilt each render -- plain buttons, nothing to animate/preserve focus for).
const chipsEl = $('chips');
// 10 department tiles, created once.
const tileEls = depts.map((name) => {
const div = document.createElement('div');
div.className = 'lm-tile';
const nameEl = document.createElement('div');
nameEl.className = 'lm-tile-name';
nameEl.textContent = name;
const pctEl = document.createElement('div');
pctEl.className = 'lm-tile-pct';
pctEl.textContent = '·';
div.appendChild(nameEl);
div.appendChild(pctEl);
$('tiles').appendChild(div);
return { root: div, nameEl, pctEl };
});
// 4 department detail bars, created once (top-4 by probability; which dept occupies which row
// changes between runs, so the row's NAME text updates, but the row/fill DOM nodes persist so
// the fill's width transition animates).
const deptBarEls = Array.from({ length: 4 }, () => {
const row = document.createElement('div');
row.className = 'lm-barrow';
const head = document.createElement('div');
head.className = 'lm-barrow-head';
const nameEl = document.createElement('div');
nameEl.className = 'lm-barrow-name';
const pctEl = document.createElement('div');
pctEl.className = 'lm-barrow-pct';
head.appendChild(nameEl);
head.appendChild(pctEl);
const track = document.createElement('div');
track.className = 'lm-bartrack';
const fill = document.createElement('div');
fill.className = 'lm-barfill';
track.appendChild(fill);
row.appendChild(head);
row.appendChild(track);
$('deptBars').appendChild(row);
return { nameEl, pctEl, fill };
});
// 3 urgency bars, created once. Names (Low/Medium/High) are fixed per language.
const urgBarEls = t.urgL.map((name) => {
const col = document.createElement('div');
col.className = 'lm-urgcol';
const pctEl = document.createElement('div');
pctEl.className = 'lm-urgpct';
const bar = document.createElement('div');
bar.className = 'lm-urgbar';
col.appendChild(pctEl);
col.appendChild(bar);
$('urgPlot').appendChild(col);
const label = document.createElement('div');
label.className = 'lm-urglabel';
label.textContent = name;
$('urgLabels').appendChild(label);
return { pctEl, bar };
});
// 4 donut segments + legend rows, created once.
const C = 2 * Math.PI * 42;
const svgNS = 'http://www.w3.org/2000/svg';
const donutSegs = t.typeL.map((_, i) => {
const circle = document.createElementNS(svgNS, 'circle');
circle.setAttribute('cx', '56'); circle.setAttribute('cy', '56'); circle.setAttribute('r', '42');
circle.setAttribute('fill', 'none'); circle.setAttribute('stroke-width', '14');
circle.setAttribute('stroke', TYPE_COLORS[i]);
circle.setAttribute('stroke-dasharray', '0 ' + C.toFixed(2));
circle.setAttribute('stroke-dashoffset', '0');
circle.style.transition = 'stroke-dasharray 700ms cubic-bezier(.2,.8,.2,1), stroke-dashoffset 700ms cubic-bezier(.2,.8,.2,1)';
$('donutSegs').appendChild(circle);
return circle;
});
const legendEls = t.typeL.map((name, i) => {
const row = document.createElement('div');
row.className = 'lm-legend-row';
const sw = document.createElement('div');
sw.className = 'lm-legend-swatch';
sw.style.background = TYPE_COLORS[i];
const nameEl = document.createElement('div');
nameEl.className = 'lm-legend-name';
nameEl.textContent = name;
const pctEl = document.createElement('div');
pctEl.className = 'lm-legend-pct';
row.appendChild(sw); row.appendChild(nameEl); row.appendChild(pctEl);
$('legend').appendChild(row);
return { pctEl };
});
// ---- state ----
let state = { sample: 0, text: null, phase: 'idle', scanIdx: -1, result: null, threshold: 80, ms: 0 };
let timers = [];
function currentText() {
return state.text ?? (state.sample != null ? t.samples[state.sample][1] : '');
}
// No resize reporting needed here: the blog's own embed harness auto-injects a
// ResizeObserver-based resize script into every embed fence's srcdoc (see BlogPost.tsx's
// EMBED_RESIZE_SCRIPT), generically, for any embed.
function render() {
const text = currentText();
const phase = state.phase;
const result = state.result;
const done = phase === 'done' && result;
const r = result || { dept: Array(10).fill(0), urg: [0, 0, 0], type: [0, 0, 0, 0] };
const win = r.dept.indexOf(Math.max(...r.dept));
const conf = v => v * 100 >= state.threshold;
const autoDept = conf(r.dept[win]);
// Textarea (never recreated -- only .value is ever set programmatically).
if (textarea.value !== text) textarea.value = text;
// Sample chips.
chipsEl.innerHTML = '';
t.samples.forEach(([title], i) => {
const on = state.sample === i && state.text == null;
const btn = document.createElement('button');
btn.type = 'button';
btn.className = 'lm-chip' + (on ? ' on' : '');
btn.innerHTML = '<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="' + ICONS[i] + '"></path></svg>' + title;
btn.addEventListener('click', () => { state = { ...state, sample: i, text: null }; run(); });
chipsEl.appendChild(btn);
});
$('charCount').textContent = text.length + ' ' + t.chars;
runLabel.textContent = phase === 'scan' ? t.running : t.run;
runBtn.disabled = !text.trim() || phase === 'scan';
runBtn.style.opacity = !text.trim() ? '0.4' : '1';
$('thresholdVal').textContent = state.threshold + (en ? '%' : ' %');
// Department tiles.
tileEls.forEach((el, i) => {
const scanning = phase === 'scan' && i === state.scanIdx;
const isWin = done && i === win;
let bg = 'rgba(11,21,38,0.55)', border = 'rgba(59,130,246,0.22)', color = done ? '#94A3B8' : '#CBD5E1', pctColor = '#64748B', borderStyle = 'solid', scale = 'scale(1)';
if (scanning) { bg = 'rgba(34,211,238,0.22)'; border = '#22D3EE'; color = '#F8FAFC'; }
if (isWin && autoDept) { bg = '#22D3EE'; border = '#22D3EE'; color = '#0F172A'; pctColor = '#0F172A'; scale = 'scale(1.03)'; }
if (isWin && !autoDept) { bg = 'rgba(148,163,184,0.16)'; border = '#CBD5E1'; color = '#F8FAFC'; pctColor = '#CBD5E1'; borderStyle = 'dashed'; scale = 'scale(1.03)'; }
el.root.style.background = bg;
el.root.style.border = '1.5px ' + borderStyle + ' ' + border;
el.root.style.transform = scale;
el.nameEl.style.color = color;
el.pctEl.style.color = pctColor;
el.pctEl.textContent = done ? pct(r.dept[i]) : '·';
});
// Verdict + timing.
const verdictEl = $('verdict');
verdictEl.style.opacity = done ? '1' : '0.35';
verdictEl.style.transform = done ? 'translateY(0)' : 'translateY(4px)';
verdictEl.style.background = done && autoDept ? 'rgba(34,211,238,0.10)' : 'rgba(148,163,184,0.08)';
verdictEl.style.borderColor = done && autoDept ? 'rgba(34,211,238,0.6)' : 'rgba(148,163,184,0.35)';
const verdictColor = done && autoDept ? '#22D3EE' : '#CBD5E1';
$('verdictDot').style.background = verdictColor;
$('verdictPct').style.color = verdictColor;
$('verdictLead').firstChild.textContent = (done ? (autoDept ? t.verdictAuto : t.verdictHuman) : t.running) + ' ';
$('verdictTarget').textContent = done ? depts[win] : '';
$('verdictPct').textContent = done ? pct(r.dept[win]) : '';
$('timing').style.opacity = done ? '1' : '0.35';
$('msVal').textContent = done ? state.ms + ' ms' : '…';
$('manualVal').textContent = en ? '~30 s' : '~30 sek.';
$('factorVal').textContent = done ? '≈ ' + fmtInt(Math.round(30000 / state.ms)) + '×' : '';
// Detail cards.
const badge = v => done ? (conf(v) ? { label: t.auto, bg: 'rgba(34,211,238,0.16)', color: '#67E8F9' } : { label: t.human, bg: 'rgba(148,163,184,0.18)', color: '#CBD5E1' }) : { label: '…', bg: 'rgba(148,163,184,0.1)', color: '#64748B' };
const applyBadge = (id, b) => { const el = $(id); el.textContent = b.label; el.style.background = b.bg; el.style.color = b.color; };
applyBadge('deptBadge', badge(r.dept[win]));
const order = r.dept.map((v, i) => [v, i]).sort((a, b) => b[0] - a[0]).slice(0, 4);
deptBarEls.forEach((el, k) => {
const [v, i] = order[k];
const isTop = k === 0;
el.nameEl.textContent = depts[i];
el.pctEl.textContent = done ? pct(v) : '';
el.nameEl.style.color = isTop ? '#F8FAFC' : '#94A3B8';
el.pctEl.style.color = isTop ? '#F8FAFC' : '#94A3B8';
el.fill.style.width = done ? (v * 100).toFixed(1) + '%' : '0%';
el.fill.style.background = isTop ? '#22D3EE' : '#475569';
});
const urgTop = r.urg.indexOf(Math.max(...r.urg));
applyBadge('urgBadge', badge(r.urg[urgTop]));
urgBarEls.forEach((el, i) => {
const isTop = i === urgTop;
el.pctEl.textContent = done ? pct(r.urg[i]) : '';
el.pctEl.style.color = isTop ? '#F8FAFC' : '#94A3B8';
el.bar.style.height = done ? Math.max(r.urg[i] * 88, 3).toFixed(1) + 'px' : '3px';
el.bar.style.background = isTop ? '#22D3EE' : '#475569';
});
const typeTopI = r.type.indexOf(Math.max(...r.type));
applyBadge('typeBadge', badge(r.type[typeTopI]));
let acc = 0;
donutSegs.forEach((circle, i) => {
const v = r.type[i];
const len = done ? v * C : 0;
circle.setAttribute('stroke-dasharray', Math.max(len - 1.5, 0).toFixed(2) + ' ' + C.toFixed(2));
circle.setAttribute('stroke-dashoffset', (-acc).toFixed(2));
acc += len;
});
$('donutPct').textContent = done ? pct(r.type[typeTopI]) : '';
$('donutName').textContent = done ? t.typeL[typeTopI] : '';
legendEls.forEach((el, i) => { el.pctEl.textContent = done ? pct(r.type[i]) : ''; });
}
function run() {
const text = currentText().trim();
if (!text) { render(); return; }
timers.forEach(clearTimeout); timers = [];
const raw = (state.sample != null && state.text == null) ? FIXED[state.sample] : classify(text);
const result = { dept: norm(raw.dept), urg: norm(raw.urg), type: norm(raw.type) };
const win = result.dept.indexOf(Math.max(...result.dept));
const steps = 13, path = [];
let prev = -1;
for (let k = 0; k < steps - 1; k++) {
let n;
do { n = Math.floor(Math.random() * 10); } while (n === prev || n === win);
path.push(n); prev = n;
}
path.push(win);
const ms = Math.round(7 + text.length / 45 + Math.random() * 6);
state = { ...state, phase: 'scan', scanIdx: path[0], result, ms };
render();
let tAcc = 0;
path.forEach((idx, k) => {
tAcc += 40 + k * k * 2.2;
timers.push(setTimeout(() => { state = { ...state, scanIdx: idx }; render(); }, tAcc));
});
timers.push(setTimeout(() => { state = { ...state, phase: 'done' }; render(); }, tAcc + 260));
}
textarea.addEventListener('input', (e) => {
state = { ...state, text: e.target.value, sample: null };
render();
});
runBtn.addEventListener('click', () => run());
thresholdSlider.value = String(state.threshold);
thresholdSlider.addEventListener('input', (e) => {
state = { ...state, threshold: +e.target.value };
render();
});
render();
run();
})();
</script>
</body>
</html>
