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

Jetpack AI: add SEO assistant PoC #40802

Merged
merged 29 commits into from
Jan 16, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
144cafe
first draft, very much hardcoded and with no real actionables, versio…
CGastrell Jan 1, 2025
19b258a
changelog
CGastrell Jan 1, 2025
75226f2
interpolate better those br
CGastrell Jan 1, 2025
6e9a428
add suspenseful typing effect for async processes
CGastrell Jan 2, 2025
7f1b55d
add more handlers and options for steps
CGastrell Jan 2, 2025
72004d7
use empty initial states
CGastrell Jan 2, 2025
c8e16b8
fix keywords confirmation message to handle only 1 keyword
CGastrell Jan 2, 2025
0cd4673
introduce async functions for generation/regeneration. Use new step p…
CGastrell Jan 2, 2025
463766b
fix initial task interpolated elements
CGastrell Jan 6, 2025
2cb4bee
do not render the assistant is the post type is not viewable
CGastrell Jan 6, 2025
d1317a2
reorganize code, add module status management and CTA states
CGastrell Jan 6, 2025
9177794
move SEO assistant CTA to Jetpack sidebar under SEO panel
CGastrell Jan 8, 2025
4f9a41b
refactor some behaviors and adjust styles towards designs
CGastrell Jan 8, 2025
6881445
refactor code to split steps, improve chat flow, add first meta handler
CGastrell Jan 9, 2025
c8214ca
clean up some commented code
CGastrell Jan 9, 2025
8136de7
temp, completed tracking is failing, need to set complete on done
CGastrell Jan 10, 2025
57883bf
reinstate completion step hook, nothing works so far
CGastrell Jan 13, 2025
5d32e93
better split of the input component
CGastrell Jan 13, 2025
8fd77c7
decouple message rendering to its own component, can be further split…
CGastrell Jan 13, 2025
6ba0a2e
change skip message copy edit
CGastrell Jan 13, 2025
7993a8c
restore wizard messages scrollbar
CGastrell Jan 13, 2025
bdddd2f
add new skip/close button, use wpicons and button components
CGastrell Jan 13, 2025
e10cca0
fix side scrolling issue
CGastrell Jan 14, 2025
fb55574
turn messages into components
CGastrell Jan 14, 2025
29941ce
translate all strings, simplify message object
CGastrell Jan 14, 2025
29a675a
trying to get messages queued
CGastrell Jan 15, 2025
e34721d
turn props to ints, failing linte
CGastrell Jan 15, 2025
27307c5
testing solutions for step tracker
CGastrell Jan 15, 2025
f69072e
cleanup commented code, use isEditedPostEmpty as a way to disable the…
CGastrell Jan 16, 2025
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: minor
Type: other

Jetpack AI: add PoC for SEO assistant, hardcoded and no actionables yet
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,14 @@ import useAICheckout from '../../../../blocks/ai-assistant/hooks/use-ai-checkout
import useAiFeature from '../../../../blocks/ai-assistant/hooks/use-ai-feature';
import useAiProductPage from '../../../../blocks/ai-assistant/hooks/use-ai-product-page';
import { getFeatureAvailability } from '../../../../blocks/ai-assistant/lib/utils/get-feature-availability';
import { isBetaExtension } from '../../../../editor';
// import { isBetaExtension } from '../../../../editor';
import JetpackPluginSidebar from '../../../../shared/jetpack-plugin-sidebar';
import { PLAN_TYPE_FREE, PLAN_TYPE_UNLIMITED, usePlanType } from '../../../../shared/use-plan-type';
import { FeaturedImage } from '../ai-image';
import { Breve, registerBreveHighlights, Highlight } from '../breve';
import { getBreveAvailability, canWriteBriefBeEnabled } from '../breve/utils/get-availability';
import Feedback from '../feedback';
import SeoAssistant from '../seo-assistant';
// import SeoAssistant from '../seo-assistant';
import TitleOptimization from '../title-optimization';
import UsagePanel from '../usage-panel';
import {
Expand Down Expand Up @@ -60,7 +60,7 @@ const isAITitleOptimizationKeywordsFeatureAvailable = getFeatureAvailability(
'ai-title-optimization-keywords-support'
);

const isSeoAssistantEnabled = getFeatureAvailability( 'ai-seo-assistant' );
// const isSeoAssistantEnabled = getFeatureAvailability( 'ai-seo-assistant' );

const JetpackAndSettingsContent = ( {
placement,
Expand All @@ -72,6 +72,14 @@ const JetpackAndSettingsContent = ( {
const { checkoutUrl } = useAICheckout();
const { productPageUrl } = useAiProductPage();
const isBreveAvailable = getBreveAvailability();
// const isViewable = useSelect( select => {
// const postTypeName = select( editorStore ).getCurrentPostType();
// const postTypeObject = ( select( coreStore ) as unknown as CoreSelect ).getPostType(
// postTypeName
// );

// return postTypeObject?.viewable;
// }, [] );

const currentTitleOptimizationSectionLabel = __( 'Optimize Publishing', 'jetpack' );
const SEOTitleOptimizationSectionLabel = __( 'Optimize Title', 'jetpack' );
Expand All @@ -89,18 +97,18 @@ const JetpackAndSettingsContent = ( {
</PanelRow>
) }

{ isSeoAssistantEnabled && (
{ /* { isSeoAssistantEnabled && isViewable && (
<PanelRow
className={ `jetpack-ai-sidebar__feature-section ${
isBetaExtension( 'ai-seo-assistant' ) ? 'is-beta-extension' : ''
}` }
>
<BaseControl __nextHasNoMarginBottom={ true }>
<BaseControl.VisualLabel>{ __( 'SEO', 'jetpack' ) }</BaseControl.VisualLabel>
<SeoAssistant busy={ false } disabled={ false } />
<SeoAssistant disabled={ false } />
</BaseControl>
</PanelRow>
) }
) } */ }

{ canWriteBriefBeEnabled() && isBreveAvailable && (
<PanelRow>
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -1,21 +1,48 @@
import { useModuleStatus } from '@automattic/jetpack-shared-extension-utils';
import { Button } from '@wordpress/components';
import { useSelect } from '@wordpress/data';
import { store as editorStore } from '@wordpress/editor';
import { useState } from '@wordpress/element';
import { __ } from '@wordpress/i18n';
import debugFactory from 'debug';
import { SeoPlaceholder } from '../../../../plugins/seo/components/placeholder';
import './style.scss';
import bigSkyIcon from './big-sky-icon.svg';
import SeoAssistantWizard from './seo-assistant-wizard';
import type { SeoAssistantProps } from './types';

const debug = debugFactory( 'jetpack-ai:seo-assistant' );

export default function SeoAssistant( { busy, disabled } ) {
export default function SeoAssistant( { disabled, onStep }: SeoAssistantProps ) {
const [ isOpen, setIsOpen ] = useState( false );
const postIsEmpty = useSelect( select => select( editorStore ).isEditedPostEmpty(), [] );
const { isLoadingModules, isChangingStatus, isModuleActive, changeStatus } =
useModuleStatus( 'seo-tools' );

debug( 'rendering seo-assistant entry point' );
return (
<div>
<p>{ __( 'Improve post engagement.', 'jetpack' ) }</p>
<Button
onClick={ () => debug( 'click' ) }
variant="secondary"
disabled={ disabled }
isBusy={ busy }
>
{ __( 'SEO Assistant', 'jetpack' ) }
</Button>
{ ( isModuleActive || isLoadingModules ) && (
<Button
onClick={ () => setIsOpen( true ) }
variant="secondary"
disabled={ isLoadingModules || isOpen || postIsEmpty || disabled }
isBusy={ isLoadingModules || isOpen }
>
<img src={ bigSkyIcon } alt={ __( 'SEO Assistant icon', 'jetpack' ) } />
&nbsp;
{ __( 'SEO Assistant', 'jetpack' ) }
</Button>
) }
{ ! isModuleActive && ! isLoadingModules && (
<SeoPlaceholder
isLoading={ isChangingStatus }
isModuleActive={ isModuleActive }
changeStatus={ changeStatus }
/>
) }
<SeoAssistantWizard isOpen={ isOpen } onStep={ onStep } close={ () => setIsOpen( false ) } />
</div>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
import { Button, Icon, Tooltip } from '@wordpress/components';
import { useState, useCallback, useEffect, useRef, useMemo } from '@wordpress/element';
import { __ } from '@wordpress/i18n';
import { next, closeSmall, chevronLeft } from '@wordpress/icons';
import debugFactory from 'debug';
import './style.scss';
import { useCompletionStep } from './use-completion-step';
import { useKeywordsStep } from './use-keywords-step';
import { useMetaDescriptionStep } from './use-meta-description-step';
import { useTitleStep } from './use-title-step';
import WizardInput from './wizard-input';
import WizardMessages from './wizard-messages';
import type { SeoAssistantProps, Step, Message } from './types';

const debug = debugFactory( 'jetpack-ai:seo-assistant-wizard' );

export default function SeoAssistantWizard( { isOpen, close, onStep }: SeoAssistantProps ) {
const [ currentStep, setCurrentStep ] = useState( 0 );
const [ messages, setMessages ] = useState< Message[] >( [] );
const messagesEndRef = useRef< HTMLDivElement >( null );
const [ isBusy, setIsBusy ] = useState( false );

const scrollToBottom = () => {
messagesEndRef.current?.scrollIntoView( { behavior: 'smooth' } );
};

useEffect( () => {
scrollToBottom();
}, [ messages ] );

const addMessage = useCallback( async ( message: Message ) => {
const newMessage = {
...message,
showIcon: message.showIcon === false ? false : ! message.isUser,
} as Message;

setMessages( prev => [ ...prev, { ...newMessage, id: `message-${ prev.length }` } ] );
}, [] );

/* Removes last message */
const removeLastMessage = () => {
setMessages( prev => prev.slice( 0, -1 ) );
};

const keywordsStep: Step = useKeywordsStep( {
addMessage,
onStep,
} );

const titleStep: Step = useTitleStep( {
addMessage,
removeLastMessage,
onStep,
contextData: keywordsStep.value,
setIsBusy,
} );

const metaStep: Step = useMetaDescriptionStep( {
addMessage,
removeLastMessage,
onStep,
setIsBusy,
} );

const completionStep: Step = useCompletionStep( {
steps: [ keywordsStep, titleStep, metaStep ],
addMessage,
} );

const steps: Step[] = useMemo(
() => [ keywordsStep, titleStep, metaStep, completionStep ],
[ keywordsStep, metaStep, titleStep, completionStep ]
);

const currentStepData = useMemo( () => steps[ currentStep ], [ steps, currentStep ] );

// initialize wizard, set completion monitors
useEffect( () => {
if ( ! isOpen ) {
return;
}
// add messageQueue.length check here for delayed messages
if ( messages.length === 0 ) {
debug( 'init' );
// Initialize with first step messages
currentStepData.messages.forEach( addMessage );
}
}, [ isOpen, currentStepData.messages, messages, addMessage ] );

const handleNext = useCallback( () => {
if ( currentStep < steps.length - 1 ) {
debug( 'moving to ' + ( currentStep + 1 ), steps[ currentStep + 1 ] );
setCurrentStep( currentStep + 1 );
// Add next step messages
// TODO: can we capture completion step here and craft the messages?
// Nothing else has worked so far to keep track of step completions
steps[ currentStep + 1 ].messages.forEach( addMessage );
steps[ currentStep + 1 ].onStart?.();
}
}, [ currentStep, steps, setCurrentStep, addMessage ] );

const handleSubmit = useCallback( async () => {
await currentStepData.onSubmit?.();
handleNext();
}, [ currentStepData, handleNext ] );

const handleBack = () => {
if ( currentStep > 0 ) {
setCurrentStep( currentStep - 1 );
// Re-add previous step messages
steps[ currentStep - 1 ].messages.forEach( message =>
addMessage( {
content: message.content,
showIcon: message.showIcon,
} )
);
}
};

const handleSkip = async () => {
await currentStepData?.onSkip?.();
handleNext();
};

// Reset states and close the wizard
const handleDone = useCallback( () => {
close();
setCurrentStep( 0 );
setMessages( [] );
steps
.filter( step => step.type !== 'completion' )
.forEach( step => step.setCompleted( false ) );
}, [ close, steps ] );

return (
isOpen && (
<div className="seo-assistant-wizard">
<div className="seo-assistant-wizard__header">
<Button variant="link" disabled={ isBusy } onClick={ handleBack }>
<Icon icon={ chevronLeft } size={ 24 } />
</Button>
<h2>{ currentStepData.title }</h2>
<div>
<Tooltip text={ __( 'Skip', 'jetpack' ) }>
<Button variant="link" disabled={ isBusy } onClick={ handleSkip }>
<Icon icon={ next } size={ 24 } />
</Button>
</Tooltip>
<Button variant="link" onClick={ handleDone }>
<Icon icon={ closeSmall } size={ 24 } />
</Button>
</div>
</div>

<div className="seo-assistant-wizard__content">
<WizardMessages currentStepData={ currentStepData } messages={ messages } />

<WizardInput
currentStepData={ currentStepData }
handleDone={ handleDone }
handleSubmit={ handleSubmit }
/>
</div>
</div>
)
);
}
Loading
Loading