· Webentwicklung · 5 minuten Lesezeit
Lowercase-Normalisierung und Duplikat-Erkennung im Tag-Input mit Chip-Animation
Tags in einer Vektordatenbank müssen konsistent sein. "Rolex" und "rolex" sind zwei verschiedene Token, beide landen im Embedding. Ein toLowerCase() und eine animierte Chip-Reaktion auf Duplikate lösen das Problem ohne UI-Reibung.

Inhalt
- Warum Normalisierung wichtig ist
- Die Normalisierung
- Das Duplikat-Problem
- Die Duplikat-Prüfung
- Das UX-Problem mit stiller Ablehnung
- Die Chip-Pulse-Animation
- querySelector innerhalb des Shadow DOM
- Das vollständige commitTagInput
- Alle Artikel der Serie
Warum Normalisierung wichtig ist
Tags landen direkt im Embedding-Text. Wie daraus im Backend ein Vektor entsteht, beschreibe ich im Artikel über Qdrant, Embeddings und den RAG-System-Aufbau. Das Backend konkateniert alle Felder zu einer Zeichenkette, die dann in einen Vektor umgewandelt wird.
const parts = [
channel ?? '',
caption ?? '',
altTexts.join(' '),
note ?? '',
tags.join(' '), // Tags are embedded directly
].filter(Boolean);Das Embedding-Modell kennt keine Tags im abstrakten Sinne. Es sieht Text-Token. “Python” und “python” sind für die meisten Tokenizer verschiedene Token oder zumindest in unterschiedlichen Embedding-Dimensionen aktiv. Zwei Posts mit dem gleichen konzeptuellen Tag landen im Vektorraum an leicht unterschiedlichen Positionen, wenn die Schreibweise inkonsistent ist.
Das beeinträchtigt Suchanfragen. Wenn der Nutzer nach “python” fragt und Hälfte der Posts das Tag als “Python” gespeichert hat, ist die Trefferquote schlechter als nötig.
Die Lösung ist einfach: .toLowerCase() in commitTagInput(). Die Grundlage für commitTagInput() habe ich im Artikel über Notiz und Tags im Capture-Overlay beschrieben.
Die Normalisierung
Vorher:
const commitTagInput = () => {
const val = tagInput.value.replace(/,/g, '').trim();
if (val) {
currentTags.push(val);
// ...
}
};Danach:
const commitTagInput = () => {
const val = tagInput.value.replace(/,/g, '').trim().toLowerCase();
if (!val) return;
// ...
};Ein Methodenaufruf. Die Auswirkung ist spürbar: Egal ob der Nutzer “React”, “REACT” oder “react” eingibt, im Array und im Qdrant-Punkt landet immer “react”.
Das Duplikat-Problem
Normalisierung auf Lowercase schafft ein neues Problem: “React” und “react” werden als gleich behandelt. Gut für die Konsistenz. Aber was passiert, wenn der Nutzer denselben Tag zweimal eingibt?
Ohne Duplikat-Erkennung landen beide im Array:
currentTags = ['react', 'typescript', 'react']; // unerwünschtDas Embedding enthält “react” zweimal. Der Qdrant-Punkt ist falsch gewichtet. Und in der UI sieht der Nutzer zwei identische Chips nebeneinander, ein UI-Signal, dass etwas nicht stimmt.
Die Duplikat-Prüfung
Der Check ist minimal:
const commitTagInput = () => {
const val = tagInput.value.replace(/,/g, '').trim().toLowerCase();
if (!val) return;
if (currentTags.includes(val)) {
// duplicate: skip
tagInput.value = '';
return;
}
currentTags.push(val);
tagInput.value = '';
renderChips();
};Array.includes() auf einem typischerweise kleinen Array von 5 bis 15 Tags ist O(n). Das ist völlig ausreichend. Eine Map oder Set wäre Overengineering für diesen Use Case.
Das UX-Problem mit stiller Ablehnung
Der Code oben clearst den Input und gibt kein Feedback. Aus Nutzerperspektive: Der Nutzer tippt “react”, drückt Enter, der Input wird geleert. Aber kein neuer Chip erscheint. Warum? Keine Ahnung.
Stille Ablehnung ist schlechtes UX-Design. Der Nutzer versteht nicht, was passiert ist.
Die Frage: Welches Signal ist eindeutig, ohne zu stören?
Eine Fehlermeldung wäre übertrieben. Ein roter Rahmen um das Eingabefeld zeigt auf das falsche Element, denn das Eingabefeld ist nicht kaputt und der Tag existiert bereits.
Das richtige Signal: Der bestehende Chip für diesen Tag reagiert. Er zeigt: “Ich bin bereits da.”
Die Chip-Pulse-Animation
Der Chip erhält für 450ms eine Klasse, die eine kurze Farbanimation auslöst:
@keyframes chipPulse {
0% {
background: #ede9fe;
color: #4f46e5;
}
35% {
background: #fca5a5;
color: #dc2626;
transform: scale(1.06);
}
100% {
background: #ede9fe;
color: #4f46e5;
transform: scale(1);
}
}
.chip-duplicate {
animation: chipPulse 0.45s ease;
}Der Chip flasht kurz rot und wächst minimal. Danach kehrt er zur normalen Farbe zurück. Der Nutzer sieht sofort: “Dieser Tag existiert bereits.” Die Animation ist schnell genug, um nicht zu nerven, aber deutlich genug, um wahrgenommen zu werden.
Die Klasse wird nach der Animation wieder entfernt, damit sie beim nächsten Duplikat erneut ausgelöst werden kann:
if (currentTags.includes(val)) {
tagInput.value = '';
const idx = currentTags.indexOf(val);
const chips = Array.from(tagsBox.querySelectorAll<HTMLElement>('.chip'));
const dupChip = chips[idx];
if (dupChip) {
dupChip.classList.add('chip-duplicate');
dupChip.addEventListener('animationend', () => dupChip.classList.remove('chip-duplicate'), { once: true });
}
return;
}{ once: true } ist wichtig. Ohne es würde ein neuer Listener bei jedem Duplikat-Versuch auf demselben Chip hinzugefügt. Nach 5 Duplikat-Versuchen auf dem gleichen Tag: 5 Listener, die alle beim nächsten animationend feuern. Mit { once: true } entfernt sich der Listener nach dem ersten Aufruf selbst.
querySelector innerhalb des Shadow DOM
Die Chip-Suche über tagsBox.querySelectorAll(".chip") ist wichtig: Sie sucht innerhalb der Shadow Root, nicht im gesamten Dokument. document.querySelectorAll(".chip") würde nichts finden, weil der Shadow DOM vom äußeren DOM getrennt ist.
// Correct: search within the shadow root
const chips = Array.from(tagsBox.querySelectorAll<HTMLElement>('.chip'));Der Index des Chips entspricht dem Index im currentTags-Array, weil renderChips() beide in derselben Reihenfolge erzeugt. chips[idx] ist also der Chip für currentTags[idx].
Das vollständige commitTagInput
const commitTagInput = () => {
const val = tagInput.value.replace(/,/g, '').trim().toLowerCase();
if (!val) return;
if (currentTags.includes(val)) {
tagInput.value = '';
const idx = currentTags.indexOf(val);
const chips = Array.from(tagsBox.querySelectorAll<HTMLElement>('.chip'));
const dupChip = chips[idx];
if (dupChip) {
dupChip.classList.add('chip-duplicate');
dupChip.addEventListener('animationend', () => dupChip.classList.remove('chip-duplicate'), { once: true });
}
return;
}
currentTags.push(val);
tagInput.value = '';
renderChips();
};Zwei Responsibilities in einer Funktion: Normalisierung und Duplikat-Schutz. Das ist kohärent, weil beide dasselbe Ziel haben: ein konsistentes Tag-Array ohne Überraschungen.
Abbildung: Der Ablauf bei Duplikat-Eingabe: Nutzer tippt “react” (bereits als Chip vorhanden), drückt Enter, der Input wird geleert, und der bestehende “react”-Chip flasht kurz rot, bevor er zur normalen Farbe zurueckkehrt.
Alle Artikel der Serie
- Vision und Systemübersicht: Chrome Extension, RAG-Architektur, Projekthintergrund: Artikel lesen
- RAG-System Aufbau: Qdrant, Embeddings, Cosine-Ähnlichkeit in TypeScript: Artikel lesen
- AI Provider Abstraktion: Ollama vs. OpenAI, Interface-Design, kein Vendor-Lock-in: Artikel lesen
- Chrome Extension MV3: Drei isolierte Laufzeitkontexte, Message Passing, Strategy Pattern: Artikel lesen
- Docker Compose Strategie: Override-Pattern, von lokal zu Azure: Artikel lesen
- Ollama lokal vs. Docker: Die Entscheidung und ihre Konsequenzen: Artikel lesen
- Ollama Auto-Pull Entrypoint: Automatisiertes Modell-Setup beim Container-Start: Artikel lesen
- tsconfig und Vite:
Node16vs.bundler, warum Vite eigene Regeln hat: Artikel lesen - Instagram Caption mit MutationObserver vollständig laden: Artikel lesen
- Chrome Extension Foundation mit Health-Dot und Retry-Queue: Artikel lesen
- Phase 2 Features: Shadow DOM Overlay, Tailwind v4, Duplicate Detection: Artikel lesen
- Race Condition bei der Plattformerkennung: Wie ein UI-Event die Instagram-Erkennung bricht: Artikel lesen
- PostId-Extraktion in zwei Instagram-Layouts: querySelector vs. Ancestor-Traversal: Artikel lesen
- Instagram Karussell vollständig erfassen mit MutationObserver: Artikel lesen
- Notiz und Tags beim Screenshot-Speichern: Artikel lesen
- Instagram Tastatur-Shortcuts blockieren Chrome Extension Eingaben: Artikel lesen
- Lowercase-Normalisierung und Duplikat-Erkennung im Tag-Input (dieser Artikel)
- Zitadel Login V2 in Docker Compose: drei versteckte Fehler: Artikel lesen
- PKCE OAuth in einer Chrome MV3 Extension: Artikel lesen
- React Frontend mit react-oidc-context und Zitadel: Artikel lesen
- Vite Build-Time-Umgebungsvariablen in Docker: Artikel lesen
Du baust ein Datenerfassungssystem auf Basis von Browser Extensions mit komplexen DOM-Interaktionen? Lass uns das gemeinsam einschätzen.



