diff --git a/projects/plugins/jetpack/changelog/update-jetpack-ai-breve-load-dictionary-from-server b/projects/plugins/jetpack/changelog/update-jetpack-ai-breve-load-dictionary-from-server new file mode 100644 index 0000000000000..5c99eb30b2351 --- /dev/null +++ b/projects/plugins/jetpack/changelog/update-jetpack-ai-breve-load-dictionary-from-server @@ -0,0 +1,4 @@ +Significance: patch +Type: other + +AI Assistant: Load dictionaries from CDN diff --git a/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/breve/features/spelling-mistakes/index.ts b/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/breve/features/spelling-mistakes/index.ts index 4db69f3c54ff5..d24202ff1662d 100644 --- a/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/breve/features/spelling-mistakes/index.ts +++ b/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/breve/features/spelling-mistakes/index.ts @@ -2,11 +2,23 @@ * External dependencies */ import { __ } from '@wordpress/i18n'; +import debugFactory from 'debug'; import nspell from 'nspell'; +/** + * Internal dependencies + */ +import getDictionary from '../../utils/get-dictionary'; /** * Types */ -import type { BreveFeatureConfig, HighlightedText, SpellChecker } from '../../types'; +import type { + BreveFeatureConfig, + SpellingDictionaryContext, + HighlightedText, + SpellChecker, +} from '../../types'; + +const debug = debugFactory( 'jetpack-ai-breve:spelling-mistakes' ); export const SPELLING_MISTAKES: BreveFeatureConfig = { name: 'spelling-mistakes', @@ -17,16 +29,46 @@ export const SPELLING_MISTAKES: BreveFeatureConfig = { }; const spellcheckers: { [ key: string ]: SpellChecker } = {}; -const spellingContexts: { - [ key: string ]: { - affix: string; - dictionary: string; - }; +const contextRequests: { + [ key: string ]: { loading: boolean; loaded: boolean; failed: boolean }; } = {}; -const loadContext = ( language: string ) => { - // TODO: Load dictionaries dynamically and save on localStorage - return spellingContexts[ language ]; +const fetchContext = async ( language: string ) => { + debug( 'Fetching spelling context from the server' ); + + try { + contextRequests[ language ] = { loading: true, loaded: false, failed: false }; + const data = await getDictionary( SPELLING_MISTAKES.name, language ); + + localStorage.setItem( + `jetpack-ai-breve-spelling-context-${ language }`, + JSON.stringify( data ) + ); + + contextRequests[ language ] = { loading: false, loaded: true, failed: false }; + debug( 'Loaded spelling context from the server' ); + } catch ( error ) { + debug( 'Failed to fetch spelling context', error ); + contextRequests[ language ] = { loading: false, loaded: false, failed: true }; + // TODO: Handle retries + } +}; + +const getContext = ( language: string ) => { + // First check if the context is already defined in local storage + const storedContext = localStorage.getItem( `jetpack-ai-breve-spelling-context-${ language }` ); + let context: SpellingDictionaryContext | null = null; + const { loading, failed } = contextRequests[ language ] || {}; + + if ( storedContext ) { + context = JSON.parse( storedContext ); + debug( 'Loaded spelling context from local storage' ); + } else if ( ! loading && ! failed ) { + // If the context is not in local storage and we haven't failed to fetch it before, try to fetch it once + fetchContext( language ); + } + + return context; }; const getSpellchecker = ( { language = 'en' }: { language?: string } = {} ) => { @@ -36,7 +78,7 @@ const getSpellchecker = ( { language = 'en' }: { language?: string } = {} ) => { // Cannot await here as the Rich Text function needs to be synchronous. // Load of the dictionary in the background if necessary and re-trigger the highlights later. - const spellingContext = loadContext( language ); + const spellingContext = getContext( language ); if ( ! spellingContext ) { return null; diff --git a/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/breve/types.ts b/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/breve/types.ts index 199ff5c12d625..1e8874d06f58c 100644 --- a/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/breve/types.ts +++ b/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/breve/types.ts @@ -114,3 +114,8 @@ export type SpellChecker = { correct: ( word: string ) => boolean; suggest: ( word: string ) => Array< string >; }; + +export type SpellingDictionaryContext = { + affix: string; + dictionary: string; +}; diff --git a/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/breve/utils/get-dictionary.ts b/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/breve/utils/get-dictionary.ts new file mode 100644 index 0000000000000..45acc8e0d5776 --- /dev/null +++ b/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/breve/utils/get-dictionary.ts @@ -0,0 +1,43 @@ +/** + * External dependencies + */ +import debugFactory from 'debug'; +/** + * Types + */ +import { SpellingDictionaryContext } from '../types'; + +const debug = debugFactory( 'jetpack-ai-breve:get-dictionary' ); + +/** + * A function that gets a Breve dictionary context object. + * + * @param {string} type + * @param {string} language + * @return {Promise} - A promise that resolves a dictionary context object. + */ +export default async function getDictionary( + type: string, + language: string +): Promise< SpellingDictionaryContext > { + debug( 'Asking dictionary for type: %s. language: %s', type, language ); + + // Randomize the server to balance the load + const counter = Math.floor( Math.random() * 3 ); + const url = `https://s${ counter }.wp.com/wp-content/lib/jetpack-ai/breve-dictionaries/${ type }/${ language }.json`; + + try { + const data = await fetch( url ); + + if ( data.status === 404 ) { + throw new Error( 'The requested dictionary does not exist' ); + } else if ( data.status !== 200 ) { + throw new Error( 'Failed to fetch dictionary' ); + } + + return data.json(); + } catch ( error ) { + debug( 'Error getting dictionary: %o', error ); + return Promise.reject( error ); + } +}