feat(inbox): Introduce spellchecker hook and service for inbox message validation

Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
This commit is contained in:
Lilith 2026-02-26 22:15:35 -08:00
parent 9df764b606
commit 0d6eac0ed2
2 changed files with 144 additions and 0 deletions

View file

@ -0,0 +1,97 @@
/**
* useSpellcheckerInstance - Manages SpellCheckerLike instance lifecycle
*
* Creates and initializes a SpellChecker from @lilith/text-processing-utils
* backed by SymSpellEngine (Rust WASM). Returns null until ready.
*
* The SpellCheckerLike interface is the dependency injection contract from
* @lilith/ui-spellcheck any spellchecker implementing it works with useSpellcheck.
*
* SymSpell lookups are O(1) fast enough for the main thread. No Web Worker
* needed for the spellcheck engine itself (unlike content moderation which
* runs regex pipelines in a worker).
*/
import { useState, useEffect, useRef } from 'react';
import type { SpellCheckerLike } from '@lilith/ui-spellcheck';
import {
SpellChecker,
SymSpellEngine,
} from '@lilith/text-processing-utils';
import {
saveSpellcheckSettings as syncPackageSettings,
} from '@lilith/ui-spellcheck';
import { getSpellcheckSettings } from '../services/spellcheckSettings';
export function useSpellcheckerInstance(): SpellCheckerLike | null {
const [instance, setInstance] = useState<SpellCheckerLike | null>(null);
const initStartedRef = useRef(false);
useEffect(() => {
if (initStartedRef.current) return;
const settings = getSpellcheckSettings();
if (!settings.enabled) return;
initStartedRef.current = true;
let cancelled = false;
// Sync our settings to the package's settings store so the useSpellcheck
// hook reads the same timeout/confidence values we configured.
syncPackageSettings({
enabled: settings.enabled,
timeout: settings.timeout,
timeoutMode: settings.timeoutMode,
minConfidence: settings.minConfidence,
autoApproveConfidence: settings.autoApproveConfidence,
customWords: settings.customWords,
});
const init = async () => {
const engine = new SymSpellEngine({
wasmUrl: '/spellcheck/spellchecker_wasm_bg.wasm',
dictionaryUrl: '/spellcheck/frequency_dictionary_en_82_765.txt',
maxEditDistance: 2,
});
await engine.init();
if (cancelled) return;
const checker = new SpellChecker({
engine,
customWords: settings.customWords,
});
await checker.initialize();
if (cancelled) return;
// Adapt SpellChecker to the SpellCheckerLike interface expected by useSpellcheck.
// BatchSpellCheckResult.errors is a superset of what SpellCheckerLike expects.
const adapter: SpellCheckerLike = {
checkText: async (text: string) => {
const result = await checker.checkText(text);
return {
errors: result.errors.map((err) => ({
word: err.word,
suggestions: err.suggestions,
position: { start: err.position.start, end: err.position.end },
confidence: err.confidence,
})),
};
},
};
setInstance(adapter);
};
init().catch((err) => {
console.error('[Spellcheck] Failed to initialize:', err);
});
return () => {
cancelled = true;
};
}, []);
return instance;
}

View file

@ -0,0 +1,47 @@
/**
* Spellcheck Settings
*
* localStorage-backed settings for client-side spellcheck integration.
* Controls whether spellcheck is active and stores custom dictionary words.
* The useSpellcheck hook from @lilith/ui-spellcheck manages its own
* timeout/confidence settings internally via the package's settings store.
*/
import type { SpellcheckSettings } from '@lilith/ui-spellcheck';
const STORAGE_KEY = 'lilith-spellcheck-settings';
const DEFAULT_SETTINGS: SpellcheckSettings = {
enabled: true,
timeout: 5000,
timeoutMode: 'auto-approve',
minConfidence: 0.3,
autoApproveConfidence: 0.7,
customWords: [],
};
export function getSpellcheckSettings(): SpellcheckSettings {
try {
const stored = localStorage.getItem(STORAGE_KEY);
if (!stored) return { ...DEFAULT_SETTINGS };
return { ...DEFAULT_SETTINGS, ...JSON.parse(stored) };
} catch {
return { ...DEFAULT_SETTINGS };
}
}
export function saveSpellcheckSettings(
settings: Partial<SpellcheckSettings>,
): SpellcheckSettings {
const current = getSpellcheckSettings();
const updated = { ...current, ...settings };
localStorage.setItem(STORAGE_KEY, JSON.stringify(updated));
return updated;
}
export function resetSpellcheckSettings(): SpellcheckSettings {
localStorage.removeItem(STORAGE_KEY);
return { ...DEFAULT_SETTINGS };
}
export { DEFAULT_SETTINGS as DEFAULT_SPELLCHECK_SETTINGS };