3 von 4 E-Mails ließen sich mit 90 % Präzision automatisch kategorisieren.
Wie viele Ihrer Kunden-E-Mails werden mindestens einmal weitergeleitet, bevor sie bei der richtigen Person landen?
Wenn ein Mitarbeiter 30 Sekunden braucht, um jede E-Mail zu lesen und einzusortieren, und Sie 500 am Tag bekommen, sind das über 4 Stunden täglich. Jeden Tag. Zeit, die direkt von dem abgeht, wofür der Kundenservice eigentlich da ist: den Kunden zu helfen.
Wir haben uns zur Aufgabe gemacht, all das zu entfernen, was Kundenservice schwerfällig macht. E-Mails, die in der falschen Warteschlange liegen. Kunden, die warten, weil niemand gesehen hat, dass es eilt. Mitarbeitende, die ihren Tag mit Sortieren statt mit Problemlösen verbringen.
Gleichzeitig liegt ein großer Teil der Lösung oft schon in Ihren eigenen Systemen. Jedes Mal, wenn ein Mitarbeiter eine E-Mail in die richtige Warteschlange verschoben, eine Priorität gesetzt oder einen Fall als Störung markiert hat, hat er im Grunde ein Trainingsbeispiel erzeugt. Die meisten Kundenserviceabteilungen sitzen auf Tausenden davon, ohne sie je zu nutzen.
Wir wollten herausfinden, wie weit man damit kommt.
Warum nicht einfach ein großes Sprachmodell nutzen?
Das ist die erste Frage, die die meisten stellen, und es ist eine gute Frage. Große Sprachmodelle wie die hinter ChatGPT sind beeindruckend im Schreiben, Zusammenfassen und Schlussfolgern. Aber Tausende E-Mails am Tag zu sortieren ist eine andere Art von Aufgabe.
Sie ähnelt eher dem, was ein erfahrener Mitarbeiter in zwei Sekunden tut: einen Blick auf die E-Mail werfen und sofort wissen, dass sie zur Buchhaltung gehört und dass es eilt. Das erfordert keinen langen Text. Es erfordert eine schnelle, verlässliche Entscheidung und ein klares Gespür dafür, wann man sich nicht sicher ist.
Ein Sprachmodell ist zum Schreiben gebaut, nicht zum Entscheiden. Es muss seine Antwort in Text umsetzen, der dann wieder interpretiert werden muss. Es ist langsamer und teurer pro E-Mail. Es kann eine falsche Antwort genauso überzeugend formulieren wie eine richtige. Und seine eigene Einschätzung, wie sicher es sich ist, deckt sich nicht unbedingt damit, wie oft es tatsächlich recht hat.
Deshalb glauben wir nicht, dass das große Sprachmodell die endgültige Antwort für den Kundenservice ist. Es ist eine der Antworten. Es ist gut darin, Antwortentwürfe zu schreiben und bei komplizierten Fällen zu helfen. Aber für die vielen schnellen, sich wiederholenden Entscheidungen, die Mitarbeitenden heute Zeit rauben, braucht es ein anderes Werkzeug.

Schnelle Entscheidungen und langsames Denken sind zwei unterschiedliche Aufgaben. Es ist sinnvoll, dafür unterschiedliche Werkzeuge zu nutzen.
Was wir gemacht haben
Wir haben ein eigenes KI-Modell für genau diesen Zweck trainiert, ein sogenanntes System-One-Modell. Der Name stammt aus der Psychologie, wo "System 1" der schnelle, intuitive Teil unseres Denkens ist. Das Modell schreibt keinen Text, sondern trifft schnelle Entscheidungen. Es liest eine E-Mail und beantwortet drei Fragen gleichzeitig:
- Welche Abteilung soll die E-Mail bekommen?
- Wie dringend ist sie?
- Um welche Art von Anliegen handelt es sich: eine Störung, eine Anfrage, ein Problem oder eine Änderung?

Das Modell schreibt keine Antwort. Es trifft drei Entscheidungen über die E-Mail und sagt, wie sicher es sich bei jeder einzelnen ist.
Das Modell hat von 26.000 Support-E-Mails auf Deutsch und Englisch gelernt, und wir haben es an 1.225 deutschen E-Mails getestet, die es nie zuvor gesehen hatte. Um zu wissen, ob die Ergebnisse tatsächlich gut waren, haben wir es unterwegs gegen die Alternativen gemessen, die eine Kundenserviceabteilung realistischerweise in Betracht ziehen würde: auf die häufigste Antwort zu raten, eine klassische statistische Methode, ein gehostetes kommerzielles Entscheidungsmodell und ein offenes Entscheidungsmodell auf Basis eines großen Sprachmodells. Alle wurden an genau denselben E-Mails getestet, damit der Vergleich fair ist.
Die Ergebnisse
Die drei Fragen erwiesen sich als sehr unterschiedlich schwierig:
| Frage | Optionen | Richtige Antwort | Automatisch handhabbar bei 90 % Präzision |
|---|---|---|---|
| Um welche Art von Anliegen es sich handelt | 4 | 83 % | 77 % |
| Wie dringend sie ist | 3 | 56 % | 3 % |
| Welche Abteilung | 10 | 46 % | 10 % |
Die letzte Spalte ist die wichtigste. Sie zeigt, bei welchem Anteil der E-Mails das Modell sich sicher genug war, um sie selbst zu bearbeiten, und bei diesen lag es 9 von 10 Mal richtig.
Bei der Frage, um welche Art von Anliegen es sich handelt, konnte das Modell 77,2 % der E-Mails automatisch mit 90 % Präzision bearbeiten. Den Rest hat das Modell an einen Menschen weitergeleitet.
Das ist ein wichtiger Unterschied. Wir versuchen nicht, ein Modell zu bauen, das unbedingt auf alles antworten muss. Wir wollen lieber ein Modell, das die E-Mails erkennt, bei denen es sich sicher genug ist, und den Rest einem Menschen überlässt.
Die Abteilungswahl und die Dringlichkeit waren deutlich schwieriger, und hier muss man es offen sagen: Keine der Methoden, die wir getestet haben, hat dabei gut abgeschnitten. Nicht die klassische Methode, nicht die großen kommerziellen Modelle und auch nicht unser eigenes. Wenn sehr unterschiedliche Methoden an derselben Decke anstoßen, liegt das Problem in den Daten und den Kategorien, nicht in den Methoden. Wir kommen später darauf zurück, warum.
Bei der Abteilungswahl konnte unser Modell trotzdem 10 % der E-Mails automatisch bei derselben Präzisionsanforderung bearbeiten. Das ist nicht viel, aber mehr als doppelt so viel wie bei der klassischen Methode.
Eines der überraschenderen Ergebnisse war, dass die großen, allgemeinen Entscheidungsmodelle ohne Training bei der Abteilungswahl tatsächlich schlechter abschnitten, als einfach die häufigste Abteilung zu wählen. Größe allein reicht nicht. Wenn es um die eigenen Kategorien und Entscheidungen eines Unternehmens geht, ist es schwer, Training auf den richtigen Daten zu schlagen.

Anteil der E-Mails, die automatisch mit 90 % Präzision bearbeitet werden können. Das große, allgemeine Modell ohne Training konnte keine einzige Abteilungswahl sicher automatisieren.
Drei Dinge, die wir nicht erwartet hatten
1. Über die Hälfte der Daten musste aussortiert werden
Der Datensatz sah auf den ersten Blick sauber aus. Aber als wir tiefer gruben, zeigte sich, dass über die Hälfte der deutschen E-Mails aus einer anderen Version stammte, mit anderen Kategorien, einer anderen Dringlichkeitsskala und fehlenden Angaben.
Hätten wir mit allem trainiert, hätte das Modell gelernt, zwei unterschiedliche Sortiersysteme miteinander zu vermischen.

Über die Hälfte der deutschen E-Mails musste aussortiert werden, weil sie einem anderen Sortiersystem folgten.
Dasselbe passiert in echten Ticket-Systemen. Kategorien werden umbenannt, Abteilungen zusammengelegt, Prioritätsskalen im Laufe der Jahre geändert. Die Qualität und Konsistenz Ihrer historischen Sortierung setzt daher eine Obergrenze dafür, was ein Modell daraus lernen kann.
Das ist vielleicht nicht so aufregend wie die Wahl eines größeren Modells, aber wahrscheinlich eines der Wichtigsten, das man zuerst in den Griff bekommen sollte.
2. Eine klassische Methode aus den 1970er-Jahren war schwerer zu schlagen, als wir dachten
Wir haben mit einer einfachen statistischen Methode verglichen, die nach den Wörtern sortiert, die in der E-Mail vorkommen. Sie trainiert in wenigen Sekunden, braucht sehr wenig Rechenleistung, und bei der Gesamtpräzision war sie fast so gut wie unser Modell.
Der Unterschied lag woanders.
Unser Modell war deutlich besser bei den seltenen Abteilungen, denen, die nur wenige E-Mails bekommen und die die einfache Methode typischerweise übersieht. Die Gesamtpräzision lag bei 44,2 % für die klassische Methode gegenüber 45,5 % für unser Modell, also praktisch kein Unterschied.
Aber wenn wir mit macro-F1 messen, das den seltenen Abteilungen dasselbe Gewicht gibt wie den großen, erhalten wir 0,26 gegenüber 0,37.

Fast dieselbe Gesamtpräzision, aber unser Modell trifft bei den seltenen Abteilungen deutlich besser.
Die Lehre daraus: Man muss gegen die einfache Alternative messen. Wenn ein fortgeschrittenes Modell eine Methode, die in wenigen Sekunden trainiert, nicht schlagen kann, gibt es wenig Grund, es zu verwenden. Aber man muss auch eine Kennzahl wählen, die das Problem widerspiegelt, das man eigentlich lösen will.
3. Am wichtigsten ist nicht, wie oft das Modell recht hat, sondern ob es weiß, wann es recht hat
Wenn die Gesamtpräzision fast unentschieden war, wie konnte unser Modell dann mehr als doppelt so viele Abteilungswahlen sicher automatisieren?
Weil sich seine eigene Selbsteinschätzung dazu nutzen lässt, zu steuern, wann es handeln darf.
Wenn das Modell sich bei einer Antwort sehr sicher ist, können wir es die E-Mail automatisch bearbeiten lassen. Wenn es unsicher ist, leiten wir sie an einen Menschen weiter. So muss das Modell nicht perfekt sein, um nützlich zu sein.
Hier kommen auch die 90 % ins Spiel. Sie legen selbst fest, wie sicher sich das Modell sein muss, bevor es auf eigene Faust handeln darf. Setzen Sie die Grenze hoch, automatisieren Sie weniger, aber mit höherer Sicherheit. Setzen Sie sie niedriger, können mehr E-Mails automatisch bearbeitet werden.

Sie legen die Grenze fest. Alles, wobei sich das Modell sicher genug ist, wird automatisch bearbeitet; der Rest geht an Mitarbeitende.
Das bedeutet, dass Automatisierung nicht bedeuten muss, Mitarbeitende zu ersetzen. Sie kann genauso gut bedeuten, den Teil der Arbeit zu entfernen, bei dem ein Mitarbeiter Zeit damit verbringt, eine E-Mail zu lesen, die richtige Warteschlange zu finden und die richtige Kategorie anzuklicken.
Warum die Abteilungswahl schwierig ist
Wenn selbst die beste Methode bei der Abteilungswahl unter der Hälfte liegt, liegt das nicht nur am Modell.
Viele der Kategorien überlappen sich: Technischer Support, Produktsupport, IT-Support und Kundenservice beschreiben oft dieselben E-Mails. Eine E-Mail über einen Drucker, der nicht funktioniert, könnte mit gutem Recht zu drei von ihnen gehören.

Die meisten Fehler passieren zwischen vier Abteilungen, die dieselben Arten von E-Mails beschreiben. Das Problem liegt in den Kategorien, nicht nur im Modell.
Das ist eine Erkenntnis, die man mitnehmen sollte: Bevor man das Routing automatisiert, müssen die Kategorien scharf genug sein, dass zwei Mitarbeitende dieselbe E-Mail einigermaßen konsistent gleich einsortieren würden.
Wenn schon Menschen uneinig sind, zu welcher Abteilung ein Fall gehört, ist es schwer zu erwarten, dass ein Modell dieses Problem allein lösen kann.
Hinzu kommt, dass die Musterlösung nicht immer richtig ist. Der Datensatz, den wir getestet haben, ist generiert und nicht von echten Mitarbeitenden sortiert worden, und wir fanden Beispiele, bei denen die Abteilung in den Daten selbst falsch war. Eine E-Mail über Office-Programme, die nicht starten wollten, lag beispielsweise unter Personalwesen. Kein Modell kann gegen eine Musterlösung gut abschneiden, die selbst falsch ist.
Deshalb schauen wir uns immer zuerst die Kategorien an, bevor wir irgendetwas trainieren. Oft liegt genau dort der größte Gewinn.
Eine Frage, die Sie jedem KI-Anbieter stellen sollten
Unterwegs haben wir auch etwas entdeckt, das wenig mit dem Modell selbst zu tun hat: Trainingsdaten.
Viele der beliebten öffentlichen Datensätze, mit denen KI-Modelle trainiert werden, haben Lizenzen oder Nutzungsbedingungen, die die kommerzielle Nutzung einschränken. Das kann für Forschung und Experimente in Ordnung sein, wird aber plötzlich relevant, wenn das Modell in einem kommerziellen Produkt eingesetzt werden soll.
Wir haben deshalb Quelle und Lizenz jedes einzelnen Datensatzes dokumentiert, mit dem die Modelle trainiert werden, die wir für Kunden bauen, und Daten aussortiert, bei denen die Bedingungen unklar waren.
Wenn Sie sich also KI-Lösungen für Ihren Kundenservice ansehen, fragen Sie:
Womit wurde das Modell trainiert, und darf das kommerziell genutzt werden?
Eine gute Antwort ist konkret. Eine vage Antwort ist es wert, genauer nachzufragen.
Was es braucht
Die Methode ist dieselbe auf Ihren eigenen Daten. Sie brauchen historische Fälle mit Warteschlange, Kategorie oder Priorität, und am besten genug Beispiele, damit die verschiedenen Arten von Anliegen tatsächlich vertreten sind.
Die Kategorien müssen nicht perfekt sein. Aber wenn sich ihre Bedeutung im Laufe der Jahre mehrfach geändert hat, muss das behandelt werden, bevor man mit dem Training beginnt.
Und dann braucht es eine Entscheidung, wo Ihre Daten verarbeitet werden. Das Modell kann auf Ihren eigenen Servern laufen oder in unserer Cloud in Dänemark. Was auch immer Sie wählen, Ihre Daten verlassen nie das Land, und Sie schicken sie nicht an einen ausländischen KI-Dienst.
Die Ergebnisse hier stammen aus einem öffentlichen Datensatz. Bei Ihren eigenen E-Mails werden sie anders aussehen, und genau deshalb lohnt es sich, sie zu messen.
Was können Ihre eigenen Daten?
Unsere Mission ist nicht, einen Chatbot vor Ihre Kunden zu stellen. Es geht darum, die Reibung hinter den Kulissen zu entfernen, damit Ihre Mitarbeitenden ihre Zeit auf die Gespräche verwenden können, die tatsächlich einen Menschen brauchen.
Wir suchen 2-3 Kundenserviceabteilungen, die dabei sein wollen herauszufinden, wie weit man damit kommt.
Wir nehmen Ihre historischen Fälle, trainieren und testen ein Modell damit und zeigen Ihnen, wie groß der Anteil Ihrer E-Mails ist, der sicher automatisch bearbeitet werden kann. Wir zeigen auch, welche Kategorien gut funktionieren, wo das Modell unsicher wird und wo Ihre bestehende Sortierung vielleicht zu unklar ist, um sie zu automatisieren.
Sie müssen uns also nicht einfach glauben, dass KI einen Unterschied machen kann.
Wir können es an Ihren eigenen Daten messen.
Schreiben Sie mir, wenn das etwas für Sie ist.
Testen Sie das Modell selbst
<!DOCTYPE html>
<html lang="de">
<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 = "de";
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>
