import { MenuItem, MenuGroup, Popover, NavigableMenu } from '@wordpress/components'; import { useMemo, useState, useEffect } from '@wordpress/element'; import { hideLoadingMessage, showLoadingMessage, showNotice, ai, removeWrappingQuotes, showGrammarCheckModal, useGrammarCheckModal, resetGrammarCheckModal, showChoiceModal, } from '@/utils'; import { showGenerateContentModal } from '@/utils/generateContentModal'; import { streamIntoBlock, regenerateBlock } from './streamIntoBlock'; import { useGenerationParams } from '@/utils/generateContentModal/paramsStore'; import { BlockEditProps } from '@/types'; import { useSettings } from '@/settings'; import { insert, toHTMLString, slice, create } from '@wordpress/rich-text'; import { useSelect, select, dispatch } from '@wordpress/data'; import { ToolbarButton } from '@/components/toolbarButton'; import { __, sprintf } from '@wordpress/i18n'; import { usePrompts } from '@/utils/ai/prompts/usePrompts'; import { useService } from '@/utils/ai/services/useService'; type RegenerateExtraProps = { text: string; feature: string; service?: { slug: string; }; prompt: string; }; type Regenerate = (options: string[], extraProps: RegenerateExtraProps) => Promise; const tones = [ { key: 'professional', label: __('Professional', 'filter-ai'), }, { key: 'informal', label: __('Informal', 'filter-ai'), }, { key: 'humorous', label: __('Humorous', 'filter-ai') }, { key: 'helpful', label: __('Helpful', 'filter-ai') }, ]; type PromptKey = | 'customise_text_rewrite_prompt' | 'customise_text_expand_prompt' | 'customise_text_condense_prompt' | 'customise_text_summarise_prompt' | 'customise_text_check_grammar_prompt' | 'customise_text_change_tone_prompt'; type OnClick = (promptKey: string, params?: Record) => Promise; export const TextToolbar = ({ attributes, setAttributes, name, clientId }: BlockEditProps) => { const [changeToneAnchor, setChangeToneAnchor] = useState(null); const [showChangeToneOptions, setShowChangeToneOptions] = useState(false); const { settings } = useSettings(); const grammarModal = useGrammarCheckModal(); const rewritePrompt = usePrompts('customise_text_rewrite_prompt'); const rewritePromptService = useService('customise_text_rewrite_prompt_service'); const expandPrompt = usePrompts('customise_text_expand_prompt'); const expandPromptService = useService('customise_text_expand_prompt_service'); const condensePrompt = usePrompts('customise_text_condense_prompt'); const condensePromptService = useService('customise_text_condense_prompt_service'); const summarisePrompt = usePrompts('customise_text_summarise_prompt'); const summarisePromptService = useService('customise_text_summarise_prompt_service'); const checkGrammarPrompt = usePrompts('customise_text_check_grammar_prompt'); const checkGrammarPromptService = useService('customise_text_check_grammar_service'); const changeTonePrompt = usePrompts('customise_text_change_tone_prompt'); const changeTonePromptService = useService('customise_text_change_tone_prompt_service'); const generateContentService = useService('generate_content_prompt_service'); const regenerateParams = useGenerationParams(clientId); const promptConfigs = useMemo( () => ({ customise_text_rewrite_prompt: { prompt: rewritePrompt, service: rewritePromptService, }, customise_text_expand_prompt: { prompt: expandPrompt, service: expandPromptService, }, customise_text_condense_prompt: { prompt: condensePrompt, service: condensePromptService, }, customise_text_summarise_prompt: { prompt: summarisePrompt, service: summarisePromptService, }, customise_text_check_grammar_prompt: { prompt: checkGrammarPrompt, service: checkGrammarPromptService, }, customise_text_change_tone_prompt: { prompt: changeTonePrompt, service: changeTonePromptService, }, }), [ rewritePrompt, expandPrompt, condensePrompt, summarisePrompt, changeTonePrompt, rewritePromptService, expandPromptService, condensePromptService, summarisePromptService, checkGrammarPromptService, changeTonePromptService, settings, ] ); const { selectionStart, selectionEnd, hasMultiSelection } = useSelect( (select) => { const { getSelectionStart, getSelectionEnd, hasMultiSelection } = select('core/block-editor'); // @ts-expect-error Type 'never' has no call signatures const _selectionStart = getSelectionStart(); // @ts-expect-error Type 'never' has no call signatures const _selectionEnd = getSelectionEnd(); // @ts-expect-error Type 'never' has no call signatures const _hasMultiSelection = hasMultiSelection(); return { selectionStart: _selectionStart, selectionEnd: _selectionEnd, hasMultiSelection: _hasMultiSelection, }; }, [attributes.content] ); const hasSelection = useMemo(() => { return selectionStart.clientId === selectionEnd.clientId && selectionStart.offset !== selectionEnd.offset; }, [selectionStart, selectionEnd]); const label = useMemo(() => { switch (name) { case 'core/heading': return __('Heading', 'filter-ai'); case 'core/list-item': return __('List Item', 'filter-ai'); default: return __('Text', 'filter-ai'); } }, [name]); const update = async (newText: string, promptKey: string) => { try { if (promptKey === 'customise_text_summarise_prompt') { await navigator.clipboard.writeText(newText); showNotice({ message: __('Summary has been copied to your clipboard', 'filter-ai') }); } else { if (hasSelection) { const content = typeof attributes.content === 'string' ? create({ text: attributes.content }) : attributes.content; const newValue = insert(content, newText, selectionStart.offset, selectionEnd.offset); setAttributes({ content: toHTMLString({ value: newValue }) }); setTimeout(() => document.getSelection()?.empty(), 0); } else { setAttributes({ content: newText }); } let message: string = sprintf(__('Your %s has been updated', 'filter-ai'), label.toLowerCase()); const { service } = promptConfigs[promptKey as PromptKey] || {}; if (service?.metadata.name) { message = sprintf( __('Your %s has been updated using %s', 'filter-ai'), label.toLowerCase(), service.metadata.name ); } showNotice({ message }); } } catch (error) { console.error(error); // @ts-expect-error Property 'message' does not exist on type '{}' showNotice({ message: error?.message || error, type: 'error' }); } }; const regenerate: Regenerate = async (options: string[], { text, feature, prompt, service }) => { try { const oldOptions = [text, ...options]; const response = await ai.customiseText(feature, text, oldOptions.join(', '), prompt, service?.slug); if (!response) { throw new Error( sprintf(__('Sorry, there has been an issue while generating your %s', 'filter-ai'), label.toLowerCase()) ); } const newOptions = JSON.parse(response); showChoiceModal({ options: newOptions, choice: '', }); } catch (error) { console.error(error); // @ts-expect-error Property 'message' does not exist on type '{}' showNotice({ message: error?.message || error, type: 'error' }); } }; const onClick: OnClick = async (promptKey, params) => { const isValidPromptKey = (key: string): key is PromptKey => { return key in promptConfigs; }; if (!isValidPromptKey(promptKey)) { console.error(`Invalid prompt key: ${promptKey}`); return; } const { prompt, service } = promptConfigs[promptKey as PromptKey] || {}; if (promptKey === 'customise_text_summarise_prompt') { showLoadingMessage(label, 'summarising'); } else if (promptKey === 'customise_text_check_grammar_prompt') { showLoadingMessage(label, 'checking grammar'); } else { showLoadingMessage(label, 'customising'); } try { const feature = `filter-ai-${promptKey.replace(/_/g, '-')}`; const content = typeof attributes.content === 'string' ? create({ text: attributes.content }) : attributes.content; if (!content) { throw new Error(__('Please provide some text', 'filter-ai')); } const text = toHTMLString({ value: hasSelection ? slice(content, selectionStart.offset, selectionEnd.offset) : content, }); let finalPrompt = prompt; if (typeof prompt !== 'string') { throw new Error( __( "There was an error preparing your prompt. Please make sure that in plugin 'Settings' your prompt is a valid string.", 'filter-ai' ) ); } if (params) { for (const key in params) { finalPrompt = finalPrompt.replace(new RegExp(`{{${key}}}`, 'g'), params[key]); } } if (promptKey === 'customise_text_check_grammar_prompt') { const correctedText = await ai.fixTextGrammar(text, finalPrompt, service?.slug); if (!correctedText) { throw new Error(sprintf(__('Sorry, there has been an issue while checking grammar', 'filter-ai'))); } showGrammarCheckModal({ originalText: text, correctedText: removeWrappingQuotes(correctedText), context: { content: attributes.content, hasSelection, selectionStart, selectionEnd, serviceName: service?.metadata.name, }, }); return; } const response = await ai.customiseText(feature, text, '', finalPrompt, service?.slug); if (!response) { throw new Error( sprintf(__('Sorry, there has been an issue while generating your %s', 'filter-ai'), label.toLowerCase()) ); } const options = JSON.parse(response); showChoiceModal({ title: __('Select Your AI Generated Text', 'filter-ai'), description: __('Choose the version that works best for your content', 'filter-ai'), label: __('Choose from these AI generated options:', 'filter-ai'), update: (newValue) => update(newValue, promptKey), regenerate: (options) => regenerate(options, { prompt: finalPrompt, text, service, feature, }), options, }); } catch (error) { console.error(error); // @ts-expect-error Property 'message' does not exist on type '{}' showNotice({ message: error?.message || error, type: 'error' }); } finally { hideLoadingMessage(); } }; useEffect(() => { const { choice, context } = grammarModal; if (choice && context) { const { content, hasSelection, selectionStart, selectionEnd, serviceName } = context; try { const blockEditor = select('core/block-editor'); const blockDispatcher = dispatch('core/block-editor') as { updateBlockAttributes: (clientId: string, attributes: Record) => void; }; const startId = selectionStart?.clientId; const endId = selectionEnd?.clientId; const sameBlock = startId && endId && startId === endId; const targetBlockId = sameBlock ? startId : endId; const targetBlock = blockEditor.getBlock(targetBlockId); if (targetBlock) { if (Object.prototype.hasOwnProperty.call(targetBlock.attributes, 'content')) { if (hasSelection) { const richContent = typeof content === 'string' ? create({ text: content }) : content; const newValue = insert(richContent, choice, selectionStart.offset, selectionEnd.offset); blockDispatcher.updateBlockAttributes(targetBlockId, { content: newValue.text ? newValue.text : toHTMLString({ value: newValue }), }); setTimeout(() => document.getSelection()?.empty(), 0); } else { blockDispatcher.updateBlockAttributes(targetBlockId, { content: choice, }); } } } let message; if (serviceName) { message = sprintf(__('Grammar has been corrected using %s', 'filter-ai'), serviceName); } else { message = __('Grammar has been corrected', 'filter-ai'); } showNotice({ message }); } catch (error) { console.error('Error applying grammar correction:', error); showNotice({ message: __('There was an issue applying the grammar correction.', 'filter-ai'), type: 'error', }); } finally { resetGrammarCheckModal(); } } }, [grammarModal.choice, grammarModal.context]); const onGenerateFromPrompt = () => { if (!clientId) return; showGenerateContentModal({ blockName: name, onSubmit: ({ prompt, keywords, length, append }) => { streamIntoBlock({ clientId, blockName: name, prompt, keywords, length, service: generateContentService?.slug, append, }); }, }); }; if ( hasMultiSelection || ![ settings?.generate_content_enabled, settings?.customise_text_rewrite_enabled, settings?.customise_text_expand_enabled, settings?.customise_text_condense_enabled, settings?.customise_text_summarise_enabled, settings?.customise_text_check_grammar_prompt, settings?.customise_text_change_tone_enabled, ].some((setting) => !!setting) ) { return null; } return ( {({ onClose }) => ( <> {settings?.generate_content_enabled && ( { onClose(); onGenerateFromPrompt(); }} > {__('Generate From Prompt', 'filter-ai')} {regenerateParams && ( { onClose(); regenerateBlock(clientId); }} > {__('Regenerate', 'filter-ai')} )} )} {settings?.customise_text_rewrite_enabled && ( { onClose(); onClick('customise_text_rewrite_prompt', { type: label.toLowerCase() }); }} > {__('Rewrite', 'filter-ai')} )} {settings?.customise_text_expand_enabled && ( { onClose(); onClick('customise_text_expand_prompt', { type: label.toLowerCase() }); }} > {__('Expand', 'filter-ai')} )} {settings?.customise_text_condense_enabled && ( { onClose(); onClick('customise_text_condense_prompt', { type: label.toLowerCase() }); }} > {__('Condense', 'filter-ai')} )} {settings?.customise_text_summarise_enabled && ( { onClose(); onClick('customise_text_summarise_prompt', { type: label.toLowerCase() }); }} > {__('Summarise', 'filter-ai')} )} {settings?.customise_text_check_grammar_enabled && ( { onClose(); onClick('customise_text_check_grammar_prompt', { type: label.toLowerCase() }); }} > {__('Check Grammar', 'filter-ai')} )} {settings?.customise_text_change_tone_enabled && ( <> { setShowChangeToneOptions(true); }} isSelected={showChangeToneOptions} // @ts-expect-error Type 'string' is not assignable to type 'ReactElement' icon="arrow-right-alt2" iconPosition="right" > {__('Change Tone', 'filter-ai')} {showChangeToneOptions && ( setShowChangeToneOptions(false)} > {tones.map((tone) => ( { onClose(); onClick('customise_text_change_tone_prompt', { tone: tone.label.toLowerCase(), type: label.toLowerCase(), }); }} > {tone.label} ))} )} )} )} ); };