Ready
Network activity
0 requests
Connected - models run on-device
No recent network activity
0 groups0 total requests
Browse blocks
Language Detector
Detect what language a piece of text is written in as you type, showing the most likely languages and how confident each guess is. You can also paste two texts and see how close they are in meaning. It all runs in your browser, and models load only when you start typing or press Compare.
Install this block
npx shadcn@latest add @localmode/ui/blocks/text/language-detectorLanguage detector
Checking WebAssembly support…
'use client';/** * @file language-detector.tsx * @description Text — Language Detector: debounced 110-language auto-detection (up to 5 candidates) plus an on-device text-embedder cosine-similarity comparison, fully on-device via MediaPipe. */import { useEffect, useRef, useState } from 'react';import { useDetectLanguage } from '@localmode/react';import { cosineSimilarity, embed, getLanguageName, type EmbeddingModel, type LanguageDetectionModel,} from '@localmode/core';import { mediapipe } from '@localmode/mediapipe';import { CapabilityGate } from '@/components/capability-gate';import { CosineSimilarityMeter } from '@/components/cosine-similarity-meter';import { ScoredResultBarList } from '@/components/scored-result-bar-list';const MIN_DETECT_LENGTH = 10;const DETECT_DEBOUNCE_MS = 400;let languageModel: LanguageDetectionModel | null = null;const getLanguageDetector = () => (languageModel ??= mediapipe.languageDetector());let embedderModel: EmbeddingModel | null = null;const getTextEmbedder = () => (embedderModel ??= mediapipe.textEmbedder());export function LanguageDetectorBlock() { return ( <div className="flex flex-col gap-3 p-4"> <p className="sr-only"> Language detector </p> <CapabilityGate requires="wasm"> <div className="flex flex-col gap-3"> <DetectionSection /> <SimilaritySection /> </div> </CapabilityGate> </div> );}function DetectionSection() { const [text, setText] = useState(''); const detect = useDetectLanguage({ model: getLanguageDetector(), maxResults: 5 }); const executeRef = useRef(detect.execute); executeRef.current = detect.execute; const resetRef = useRef(detect.reset); resetRef.current = detect.reset; useEffect(() => { if (text.trim().length < MIN_DETECT_LENGTH) { resetRef.current(); return; } const id = window.setTimeout(() => void executeRef.current(text), DETECT_DEBOUNCE_MS); return () => window.clearTimeout(id); }, [text]); const candidates = (detect.data?.languages ?? []).map((l) => ({ code: l.languageCode, name: getLanguageName(l.languageCode), confidence: l.confidence, })); const status = detect.isLoading ? 'detecting' : detect.error ? 'error' : candidates.length > 0 ? 'done' : 'idle'; return ( <section className="grid items-start gap-3 rounded-lg border border-border p-4 lg:grid-cols-[1fr_1fr]"> <div className="flex flex-col gap-2"> <p className="text-sm font-medium">Language detection</p> <textarea aria-label="Text to detect language" value={text} onChange={(e) => setText(e.target.value)} rows={4} placeholder="Type or paste text in any language… e.g. Bonjour le monde" className="w-full resize-y rounded-md border border-border bg-background px-3 py-2 text-sm outline-none focus-visible:ring-[3px] focus-visible:ring-ring/50" /> <p className="text-xs text-muted-foreground"> Detection runs automatically after {MIN_DETECT_LENGTH} characters. Supports 110 languages; the model downloads on first detection. </p> {detect.error && ( <p className="text-xs text-destructive"> {detect.error.message} </p> )} </div> <div className="flex flex-col gap-2"> <p className="text-sm font-medium">Detected languages</p> <p data-status={status} className="sr-only" > {status} </p> <span data-code={candidates[0]?.code ?? ''} className="sr-only" > {candidates[0]?.code ?? ''} </span> <div> <ScoredResultBarList results={candidates.map((c) => ({ label: `${c.name} (${c.code})`, score: c.confidence, }))} isLoading={detect.isLoading} emptyState={`Type at least ${MIN_DETECT_LENGTH} characters to detect the language.`} /> </div> </div> </section> );}function SimilaritySection() { const [textA, setTextA] = useState(''); const [textB, setTextB] = useState(''); const [similarity, setSimilarity] = useState<number | null>(null); const [isComparing, setIsComparing] = useState(false); const [error, setError] = useState<string | null>(null); const compare = async () => { if (!textA.trim() || !textB.trim() || isComparing) return; setIsComparing(true); setError(null); try { const model = getTextEmbedder(); const [a, b] = await Promise.all([ embed({ model, value: textA }), embed({ model, value: textB }), ]); setSimilarity(cosineSimilarity(a.embedding, b.embedding)); } catch (err) { setError(err instanceof Error ? err.message : String(err)); } finally { setIsComparing(false); } }; return ( <section className="flex flex-col gap-3 rounded-lg border border-border p-4"> <div> <p className="text-sm font-medium">Text similarity</p> <p className="text-xs text-muted-foreground"> Both texts are embedded with the MediaPipe text embedder (Universal Sentence Encoder); the meter shows their cosine similarity. </p> </div> <div className="grid gap-2 lg:grid-cols-2"> <textarea aria-label="First text to compare" value={textA} onChange={(e) => setTextA(e.target.value)} rows={3} placeholder="Enter the first sentence…" className="w-full resize-y rounded-md border border-border bg-background px-3 py-2 text-sm outline-none focus-visible:ring-[3px] focus-visible:ring-ring/50" /> <textarea aria-label="Second text to compare" value={textB} onChange={(e) => setTextB(e.target.value)} rows={3} placeholder="Enter the second sentence…" className="w-full resize-y rounded-md border border-border bg-background px-3 py-2 text-sm outline-none focus-visible:ring-[3px] focus-visible:ring-ring/50" /> </div> <div className="flex flex-wrap items-center gap-3"> <button type="button" onClick={() => void compare()} disabled={!textA.trim() || !textB.trim() || isComparing} className="inline-flex h-8 items-center rounded-md bg-primary px-3 text-sm font-medium text-primary-foreground transition-colors hover:bg-primary/90 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:opacity-50" > {isComparing ? 'Comparing…' : 'Compare similarity'} </button> {(!textA.trim() || !textB.trim()) && !isComparing && ( <span className="text-xs text-muted-foreground"> Enter text in both boxes to compare. </span> )} {error && ( <p className="text-xs text-destructive"> {error} </p> )} </div> {similarity != null && ( <div data-score={similarity.toFixed(4)}> <CosineSimilarityMeter similarity={similarity} caption="text A ↔ text B" /> </div> )} </section> );}