Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

AI Excerpt: render AI excerpt via core slot #33529

Closed
wants to merge 5 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Significance: patch
Type: enhancement

AI Excerpt: render AI excerpt via core slot
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,8 @@ export type AiExcerptControlProps = {

model?: AiModelTypeProp;
onModelChange?: ( model: AiModelTypeProp ) => void;

help?: string | React.ReactNode | null;
};

import './style.scss';
Expand All @@ -74,16 +76,15 @@ export function AiExcerptControl( {

tone,
onToneChange,

help,
}: AiExcerptControlProps ) {
const [ isSettingActive, setIsSettingActive ] = React.useState( false );

function toggleSetting() {
setIsSettingActive( prev => ! prev );
}

// const langLabel = language || __( 'Language', 'jetpack' );
// const toneLabel = tone || __( 'Tone', 'jetpack' );

const lang = language?.split( ' ' )[ 0 ];
const langLabel = LANGUAGE_MAP[ lang ]?.label || __( 'Language', 'jetpack' );
const toneLabel = PROMPT_TONES_MAP[ tone ]?.label || __( 'Tone', 'jetpack' );
Expand All @@ -92,7 +93,7 @@ export function AiExcerptControl( {
<div className="jetpack-ai-generate-excerpt-control">
<BaseControl
className="jetpack-ai-generate-excerpt-control__header"
label={ __( 'Settings', 'jetpack' ) }
label={ __( 'AI settings', 'jetpack' ) }
>
<Button
label={ __( 'Advanced AI options', 'jetpack' ) }
Expand Down Expand Up @@ -127,10 +128,13 @@ export function AiExcerptControl( {
onChange={ onWordsNumberChange }
min={ minWords }
max={ maxWords }
help={ __(
'Sets the desired length in words for the auto-generated excerpt. The final count may vary due to how AI works.',
'jetpack'
) }
help={
help ||
__(
'The actual length may vary as the AI strives to generate coherent and meaningful content',
'jetpack'
)
}
showTooltip={ false }
disabled={ disabled }
/>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,4 @@
import { registerJetpackPlugin } from '@automattic/jetpack-shared-extension-utils';
import { dispatch } from '@wordpress/data';
import { store as editPostStore } from '@wordpress/edit-post';
import { store as editorStore } from '@wordpress/editor';
import { addFilter } from '@wordpress/hooks';
import debugFactory from 'debug';
import metadata from '../../blocks/ai-assistant/block.json';
Expand Down Expand Up @@ -41,18 +38,6 @@ function extendAiContentLensFeatures( settings, name ) {
registerJetpackPlugin( aiExcerptPluginName, aiExcerptPluginSettings );
debug( 'Registered AI Excerpt plugin' );

// check if the removeEditorPanel function exists in the editorStore.
// íf not, look for it in the editPostStore.
// @todo: remove this once Jetpack requires WordPres 6.5,
// where the removeEditorPanel function will be available in the editorStore.
const removeEditorPanel = dispatch( editorStore )?.removeEditorPanel
? dispatch( editorStore )?.removeEditorPanel
: dispatch( editPostStore )?.removeEditorPanel;

// Remove the excerpt panel by dispatching an action.
removeEditorPanel( 'post-excerpt' );
debug( 'Removed the post-excerpt panel' );

return settings;
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,17 +1,25 @@
/**
* External dependencies
*/
import { useAiSuggestions } from '@automattic/jetpack-ai-client';
import {
useAiSuggestions,
AI_MODEL_GPT_4,
ERROR_QUOTA_EXCEEDED,
} from '@automattic/jetpack-ai-client';
import {
isAtomicSite,
isSimpleSite,
useAnalytics,
} from '@automattic/jetpack-shared-extension-utils';
import { TextareaControl, ExternalLink, Button, Notice, BaseControl } from '@wordpress/components';
import { useDispatch, useSelect } from '@wordpress/data';
import { PluginDocumentSettingPanel } from '@wordpress/edit-post';
import { dispatch, useDispatch, useSelect } from '@wordpress/data';
import {
PluginDocumentSettingPanel,
// eslint-disable-next-line wpcalypso/no-unsafe-wp-apis
__experimentalPluginPostExcerpt as PluginPostExcerpt,
} from '@wordpress/edit-post';
import { store as editorStore, PostTypeSupportCheck } from '@wordpress/editor';
import { useState, useEffect, useCallback } from '@wordpress/element';
import { createInterpolateElement, useState, useEffect, useCallback } from '@wordpress/element';
import { __, sprintf, _n } from '@wordpress/i18n';
import { count } from '@wordpress/wordcount';
/**
Expand Down Expand Up @@ -299,14 +307,208 @@ ${ postContent }
);
}

export const PluginDocumentSettingPanelAiExcerpt = () => (
<PostTypeSupportCheck supportKeys="excerpt">
<PluginDocumentSettingPanel
className={ isBetaExtension( 'ai-content-lens' ) ? 'is-beta-extension inset-shadow' : '' }
name="ai-content-lens-plugin"
title={ __( 'Excerpt', 'jetpack' ) }
>
<AiPostExcerpt />
</PluginDocumentSettingPanel>
</PostTypeSupportCheck>
);
function PostExcerptAiExtension() {
const { excerpt, postId } = useSelect( select => {
const { getEditedPostAttribute, getCurrentPostId } = select( editorStore );

return {
excerpt: getEditedPostAttribute( 'excerpt' ) ?? '',
postId: getCurrentPostId() ?? 0,
};
}, [] );

const { editPost } = useDispatch( 'core/editor' );

// Post excerpt words number
const [ excerptWordsNumber, setExcerptWordsNumber ] = useState( 50 );

const [ language, setLanguage ] = useState< LanguageProp >();
const [ tone, setTone ] = useState< ToneProp >();
const [ model, setModel ] = useState< AiModelTypeProp >( AI_MODEL_GPT_4 );

const { request, stopSuggestion, requestingState, error, reset } = useAiSuggestions( {
onSuggestion: freshExcerpt => {
editPost( { excerpt: freshExcerpt } );
},
} );

// Cancel and reset AI suggestion when the component is unmounted
useEffect( () => {
return () => {
stopSuggestion();
reset();
};
}, [] ); // eslint-disable-line react-hooks/exhaustive-deps

// Pick raw post content
const postContent = useSelect(
select => {
const content = select( editorStore ).getEditedPostContent();
if ( ! content ) {
return '';
}

// return turndownService.turndown( content );
const document = new window.DOMParser().parseFromString( content, 'text/html' );

const documentRawText = document.body.textContent || document.body.innerText || '';

// Keep only one break line (\n) between blocks.
return documentRawText.replace( /\n{2,}/g, '\n' ).trim();
},
[ postId ]
);

// Show custom prompt number of words
const currentExcerpt = excerpt;
const numberOfWords = count( currentExcerpt, 'words' );
const help = createInterpolateElement(
sprintf(
// Translators: %1$s is the number of words in the excerpt.
_n(
'The actual length may vary as the AI strives to generate coherent and meaningful content. Current length: <strong>%1$s</strong> word',
'The actual length may vary as the AI strives to generate coherent and meaningful content. Current length: <strong>%1$s</strong> words',
numberOfWords,
'jetpack'
),
numberOfWords
),
{
strong: <strong />,
}
);

const isGenerateButtonDisabled =
requestingState === 'requesting' || requestingState === 'suggesting';

const isBusy = requestingState === 'requesting' || requestingState === 'suggesting';

/**
* Request AI for a new excerpt.
*
* @returns {void}
*/
function requestExcerpt(): void {
// Reset suggestion state
reset();

const messageContext: ContentLensMessageContextProps = {
type: 'ai-content-lens',
contentType: 'post-excerpt',
postId,
words: excerptWordsNumber,
language,
tone,
content: `Post content:
${ postContent }
`,
};

const prompt = [
{
role: 'jetpack-ai',
context: messageContext,
},
];

request( prompt, { feature: 'jetpack-ai-content-lens', model } );
}
const isQuotaExceeded = error?.code === ERROR_QUOTA_EXCEEDED;

// Set the docs link depending on the site type
const docsLink =
isAtomicSite() || isSimpleSite()
? __( 'https://wordpress.com/support/excerpts/', 'jetpack' )
: __( 'https://jetpack.com/support/create-better-post-excerpts-with-ai/', 'jetpack' );

return (
<div className="jetpack-ai-post-excerpt">
<div className="jetpack-generated-excerpt__ai-container">
{ error?.code && error.code !== 'error_quota_exceeded' && (
<Notice
status={ error.severity }
isDismissible={ false }
className="jetpack-ai-assistant__error"
>
{ error.message }
</Notice>
) }

{ isQuotaExceeded && <UpgradePrompt /> }

<AiExcerptControl
words={ excerptWordsNumber }
onWordsNumberChange={ wordsNumber => {
setExcerptWordsNumber( wordsNumber );
} }
language={ language }
onLanguageChange={ newLang => {
setLanguage( newLang );
} }
tone={ tone }
onToneChange={ newTone => {
setTone( newTone );
} }
model={ model }
onModelChange={ newModel => {
setModel( newModel );
} }
disabled={ isBusy || isQuotaExceeded }
help={ help }
/>

<BaseControl
help={
! postContent?.length ? __( 'Add content to generate an excerpt.', 'jetpack' ) : null
}
>
<div className="jetpack-generated-excerpt__generate-buttons-container">
<Button
onClick={ requestExcerpt }
variant="secondary"
isBusy={ isBusy }
disabled={ isGenerateButtonDisabled || isQuotaExceeded || ! postContent }
>
{ __( 'Generate', 'jetpack' ) }
</Button>
</div>
</BaseControl>

<ExternalLink href={ docsLink }>
{ __( 'AI excerpts documentation', 'jetpack' ) }
</ExternalLink>
</div>
</div>
);
}

export const PluginDocumentSettingPanelAiExcerpt = () => {
// Check if PluginPostExcerpt Slot is available.
if ( typeof PluginPostExcerpt !== 'undefined' ) {
return (
<PluginPostExcerpt>
<PostExcerptAiExtension />
</PluginPostExcerpt>
);
}

/*
* The following implementation should be removed
* once the PluginPostExcerpt Slot is available in core.
*/

// Remove the excerpt panel by dispatching an action.
dispatch( 'core/edit-post' )?.removeEditorPanel( 'post-excerpt' );

return (
<PostTypeSupportCheck supportKeys="excerpt">
<PluginDocumentSettingPanel
className={ isBetaExtension( 'ai-content-lens' ) ? 'is-beta-extension inset-shadow' : '' }
name="ai-content-lens-plugin"
title={ __( 'Excerpt', 'jetpack' ) }
>
<AiPostExcerpt />
</PluginDocumentSettingPanel>
</PostTypeSupportCheck>
);
};
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
.jetpack-ai-post-excerpt .components-textarea-control__input {
.jetpack-ai-post-excerpt {
width: 100%;
margin-bottom: 10px;
}

.jetpack-generated-excerpt__ai-container {
Expand All @@ -13,10 +12,9 @@

.jetpack-generated-excerpt__generate-buttons-container {
display: flex;
justify-content: end;
margin-bottom: 10px;
gap: 12px;
justify-content:space-around;
justify-content: flex-end;
}

// Upgrade banner
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,13 @@
.components-base-control__field {
display: flex;
justify-content: space-between;
margin-bottom: 12px;
}

.components-base-control__label {
margin-bottom: 0;
}
}

.components-base-control {
margin-bottom: 12px;
}
}

.ai-model-selector-control__radio-control .components-flex {
Expand Down
Loading