feat(inbox): ✨ Introduce spellchecker hook and service for inbox message validation
Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
This commit is contained in:
parent
9df764b606
commit
0d6eac0ed2
2 changed files with 144 additions and 0 deletions
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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 };
|
||||
Loading…
Add table
Reference in a new issue