From 0d6eac0ed27235a3d2efb2dbba3a67cd128f9798 Mon Sep 17 00:00:00 2001 From: Lilith Date: Thu, 26 Feb 2026 22:15:35 -0800 Subject: [PATCH] =?UTF-8?q?feat(inbox):=20=E2=9C=A8=20Introduce=20spellche?= =?UTF-8?q?cker=20hook=20and=20service=20for=20inbox=20message=20validatio?= =?UTF-8?q?n?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Lilith Autocommit --- .../inbox/hooks/useSpellcheckerInstance.ts | 97 +++++++++++++++++++ .../inbox/services/spellcheckSettings.ts | 47 +++++++++ 2 files changed, 144 insertions(+) create mode 100644 features/messaging/frontend-public/src/features/inbox/hooks/useSpellcheckerInstance.ts create mode 100644 features/messaging/frontend-public/src/features/inbox/services/spellcheckSettings.ts diff --git a/features/messaging/frontend-public/src/features/inbox/hooks/useSpellcheckerInstance.ts b/features/messaging/frontend-public/src/features/inbox/hooks/useSpellcheckerInstance.ts new file mode 100644 index 000000000..bb341c440 --- /dev/null +++ b/features/messaging/frontend-public/src/features/inbox/hooks/useSpellcheckerInstance.ts @@ -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(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; +} diff --git a/features/messaging/frontend-public/src/features/inbox/services/spellcheckSettings.ts b/features/messaging/frontend-public/src/features/inbox/services/spellcheckSettings.ts new file mode 100644 index 000000000..5d1d58928 --- /dev/null +++ b/features/messaging/frontend-public/src/features/inbox/services/spellcheckSettings.ts @@ -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 { + 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 };