From 144cafea22437d13d7f13a69ff8114bf1bf3b2b1 Mon Sep 17 00:00:00 2001 From: Christian Gastrell Date: Wed, 1 Jan 2025 14:07:58 -0300 Subject: [PATCH 01/29] first draft, very much hardcoded and with no real actionables, version of the SEO assistant --- .../components/seo-assistant/index.tsx | 407 +++++++++++++++++- .../components/seo-assistant/style.scss | 229 ++++++++++ 2 files changed, 623 insertions(+), 13 deletions(-) create mode 100644 projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/seo-assistant/style.scss diff --git a/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/seo-assistant/index.tsx b/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/seo-assistant/index.tsx index 436af29236eac..5794408e8ac26 100644 --- a/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/seo-assistant/index.tsx +++ b/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/seo-assistant/index.tsx @@ -1,21 +1,402 @@ -import { Button } from '@wordpress/components'; -import { __ } from '@wordpress/i18n'; +import { Button, TextControl } from '@wordpress/components'; +import { + useState, + useCallback, + useEffect, + useRef, + createInterpolateElement, +} from '@wordpress/element'; +import { __, sprintf } from '@wordpress/i18n'; +import clsx from 'clsx'; import debugFactory from 'debug'; +import './style.scss'; + +type StepType = 'input' | 'options' | 'completion'; + +interface Message { + id: string; + content: string | React.ReactNode; + isUser?: boolean; +} + +interface Option { + id: string; + content: string; + selected?: boolean; +} + +interface BaseStep { + id: string; + title: string; + messages: string[] | React.ReactNode[]; + type: StepType; +} + +interface InputStep extends BaseStep { + type: 'input'; + placeholder: string; + onSubmit: ( value: string ) => void; +} + +interface OptionsStep extends BaseStep { + type: 'options'; + options: Option[]; + onSelect: ( option: Option ) => void; + onSubmit?: () => void; + onRegenerate?: () => void; +} + +interface CompletionStep extends BaseStep { + type: 'completion'; +} + +type Step = InputStep | OptionsStep | CompletionStep; + +interface SeoAssistantProps { + busy?: boolean; + disabled?: boolean; + onStep?: ( data: { value: string | Option | null } ) => void; +} const debug = debugFactory( 'jetpack-ai:seo-assistant' ); -export default function SeoAssistant( { busy, disabled } ) { +export default function SeoAssistant( { busy, disabled, onStep }: SeoAssistantProps ) { + const [ isOpen, setIsOpen ] = useState( false ); + const [ currentStep, setCurrentStep ] = useState( 0 ); + const [ keywords, setKeywords ] = useState( '' ); + const [ selectedTitle, setSelectedTitle ] = useState< string >(); + const [ selectedMetaDescription, setSelectedMetaDescription ] = useState< string >(); + const [ messages, setMessages ] = useState< Message[] >( [] ); + const messagesEndRef = useRef< HTMLDivElement >( null ); + const [ titleOptions, setTitleOptions ] = useState< Option[] >( [ + { + id: '1', + content: 'A Photo Gallery for Gardening Enthusiasths: Flora Guide', + }, + { + id: '2', + content: 'Flora Guide: Beautiful Photos of Flowers and Plants for Gardening Enthusiasts', + }, + ] ); + + const [ metaDescriptionOptions, setMetaDescriptionOptions ] = useState< Option[] >( [ + { + id: 'meta-1', + content: + 'Explore breathtaking flower and plant photography in our Flora Guide, featuring tips and inspiration for gardening and plant enthusiasts to enhance their outdoor spaces.', + }, + ] ); + + const scrollToBottom = () => { + messagesEndRef.current?.scrollIntoView( { behavior: 'smooth' } ); + }; + + useEffect( () => { + scrollToBottom(); + }, [ messages ] ); + + const addMessage = ( content: string | React.ReactNode, isUser = false ) => { + setMessages( prev => [ + ...prev, + { + id: `message-${ prev.length }`, + content, + isUser, + }, + ] ); + }; + + const handleKeywordsSubmit = useCallback( + ( value: string ) => { + setKeywords( value ); + addMessage( value, true ); + const keywordlist = value + .split( ',' ) + .map( k => k.trim() ) + .reduce( ( acc, curr, i, arr ) => { + if ( i === arr.length - 1 ) { + return `${ acc } & ${ curr }`; + } + return i === 0 ? curr : `${ acc }, ${ curr }`; + }, '' ); + const message = createInterpolateElement( + /* Translators: wrapped string is list of keywords user has entered */ + sprintf( __( `Got it! You're targeting %s. ✨✅`, 'jetpack' ), keywordlist ), + { + b: , + } + ); + addMessage( message ); + if ( onStep ) { + onStep( { value: value } ); + } + }, + [ onStep ] + ); + + const handleTitleSelect = useCallback( ( option: Option ) => { + setSelectedTitle( option.content ); + setTitleOptions( prev => + prev.map( opt => ( { + ...opt, + selected: opt.id === option.id, + } ) ) + ); + }, [] ); + + const handleTitleRegenerate = useCallback( () => { + // This would typically be an async call to generate new titles + debug( 'Regenerating titles...' ); + }, [] ); + + const handleTitleSubmit = useCallback( () => { + addMessage( selectedTitle, true ); + addMessage( __( 'Title updated! ✅', 'jetpack' ) ); + if ( onStep ) { + onStep( { value: selectedTitle } ); + } + }, [ selectedTitle, onStep ] ); + + const handleMetaDescriptionSelect = useCallback( ( option: Option ) => { + setSelectedMetaDescription( option.content ); + setMetaDescriptionOptions( prev => + prev.map( opt => ( { + ...opt, + selected: opt.id === option.id, + } ) ) + ); + }, [] ); + + const handleMetaDescriptionSubmit = useCallback( () => { + addMessage( selectedMetaDescription, true ); + addMessage( __( 'Meta description updated! ✅', 'jetpack' ) ); + if ( onStep ) { + onStep( { value: selectedMetaDescription } ); + } + }, [ selectedMetaDescription, onStep ] ); + + const handleDone = useCallback( () => { + setIsOpen( false ); + setCurrentStep( 0 ); + setMessages( [] ); + }, [] ); + + const steps: Step[] = [ + { + id: 'keywords', + title: __( 'Optimise for SEO', 'jetpack' ), + messages: [ + __( "Hi there! 👋 Let's optimise your blog post for SEO.", 'jetpack' ), + createInterpolateElement( + __( + "Here's what we can improve:
1. Keywords
2. Title
3. Meta description", + 'jetpack' + ), + { br:
} + ), + __( 'To start, please enter 1–3 focus keywords that describe your blog post.', 'jetpack' ), + ], + type: 'input', + placeholder: __( 'Photography, plants', 'jetpack' ), + onSubmit: handleKeywordsSubmit, + }, + { + id: 'title', + title: __( 'Optimise Title', 'jetpack' ), + messages: [ + __( + "Let's optimise your title. Here are two suggestions based on your keywords. Select the one you prefer:", + 'jetpack' + ), + ], + type: 'options', + options: titleOptions, + onSelect: handleTitleSelect, + onSubmit: handleTitleSubmit, + onRegenerate: handleTitleRegenerate, + }, + { + id: 'meta', + title: __( 'Add meta description', 'jetpack' ), + messages: [ + __( "Now, let's optimize your meta description. Here's a suggestion:", 'jetpack' ), + ], + type: 'options', + options: metaDescriptionOptions, + onSelect: handleMetaDescriptionSelect, + onSubmit: handleMetaDescriptionSubmit, + onRegenerate: handleTitleRegenerate, // Reuse the same handler for now + }, + { + id: 'completion', + title: __( 'Your post is SEO-ready', 'jetpack' ), + messages: [ + __( "Here's your updated checklist:", 'jetpack' ), + createInterpolateElement( + __( '✅ Keywords
✅ Title
✅ Meta description', 'jetpack' ), + { br:
} + ), + createInterpolateElement( + __( + 'SEO optimization complete! 🎉
Your blog post is now search-engine friendly.
nHappy blogging! 😊', + 'jetpack' + ), + { br:
} + ), + ], + type: 'completion', + }, + ]; + + const currentStepData = steps[ currentStep ]; + + useEffect( () => { + if ( isOpen && messages.length === 0 ) { + // Initialize with first step messages + currentStepData.messages.forEach( message => addMessage( message ) ); + } + }, [ isOpen, currentStepData.messages, messages ] ); + + const handleNext = () => { + if ( currentStep < steps.length - 1 ) { + debug( 'moving to ' + ( currentStep + 1 ), steps[ currentStep + 1 ] ); + setCurrentStep( currentStep + 1 ); + // Add next step messages + steps[ currentStep + 1 ].messages.forEach( message => addMessage( message ) ); + } + }; + + const handleBack = () => { + if ( currentStep > 0 ) { + setCurrentStep( currentStep - 1 ); + // Re-add previous step messages + steps[ currentStep - 1 ].messages.forEach( message => addMessage( message ) ); + } + }; + + const handleSkip = () => { + setIsOpen( false ); + setCurrentStep( 0 ); + setMessages( [] ); + }; + + if ( ! isOpen ) { + return ( +
+

{ __( 'Improve post engagement.', 'jetpack' ) }

+ +
+ ); + } + + const renderCurrentInput = () => { + if ( currentStepData.type === 'input' ) { + return ( +
+ + +
+ ); + } + + if ( currentStepData.type === 'options' ) { + const selectedOption = currentStepData.options.find( opt => opt.selected ); + + return ( +
+ { currentStepData.options.map( option => ( + + ) ) } +
+ + { selectedOption && ( + + ) } +
+
+ ); + } + + if ( currentStepData.type === 'completion' ) { + return ( +
+ +
+ ); + } + + return null; + }; + return ( -
-

{ __( 'Improve post engagement.', 'jetpack' ) }

- +
+
+ +

{ currentStepData.title }

+ +
+ +
+
+ { messages.map( message => ( +
+ { message.content } +
+ ) ) } +
+
+ +
{ renderCurrentInput() }
+
); } diff --git a/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/seo-assistant/style.scss b/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/seo-assistant/style.scss new file mode 100644 index 0000000000000..55bf0bae0a89d --- /dev/null +++ b/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/seo-assistant/style.scss @@ -0,0 +1,229 @@ +.seo-assistant-wizard { + position: fixed; + bottom: 32px; + left: 50%; + transform: translateX(-50%); + width: 90%; + max-width: 600px; + height: 600px; + background: white; + border-radius: 16px; + box-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1); + z-index: 1000; + display: flex; + flex-direction: column; + + &__header { + flex: 0 0 auto; + display: flex; + align-items: center; + justify-content: space-between; + padding: 16px; + border-bottom: 1px solid #e5e7eb; + background: white; + border-radius: 16px 16px 0 0; + + h2 { + margin: 0; + font-size: 16px; + font-weight: 600; + } + + button { + background: none; + border: none; + cursor: pointer; + padding: 8px; + color: #6b7280; + + &:hover { + color: #374151; + } + } + } + + &__content { + flex: 1 1 auto; + display: flex; + flex-direction: column; + height: 100%; + overflow: hidden; + } + + &__messages { + flex: 1 1 auto; + display: flex; + flex-direction: column; + gap: 16px; + padding: 16px; + overflow-y: auto; + scroll-behavior: smooth; + align-items: flex-start; + + /* Hide scrollbar for Chrome, Safari and Opera */ + &::-webkit-scrollbar { + display: none; + } + + /* Hide scrollbar for IE, Edge and Firefox */ + -ms-overflow-style: none; + scrollbar-width: none; + } + + &__message { + padding: 12px 16px; + border-radius: 16px; + background: #e0e7ff; + white-space: pre-line; + animation: messageAppear 0.3s ease-out; + font-size: 14px; + line-height: 1.5; + + &.is-user { + background: #f3f4f6; + align-self: flex-end; + border-bottom-right-radius: 4px; + } + + &:not(.is-user) { + border-bottom-left-radius: 4px; + } + } + + &__input-container { + flex: 0 0 auto; + padding: 16px; + background: white; + border-top: 1px solid #e5e7eb; + border-radius: 0 0 16px 16px; + } + + &__input { + display: flex; + gap: 8px; + + .components-base-control { + flex-grow: 1; + } + .components-text-control__input { + border-radius: 24px; + padding: 8px 16px; + } + } + + &__submit { + border-radius: 20px; + max-height: 34px; + } + + &__options { + display: flex; + flex-direction: column; + gap: 12px; + } + + &__option { + width: 100%; + padding: 16px; + background: #f3f4f6; + border: 2px solid transparent; + border-radius: 12px; + cursor: pointer; + text-align: left; + font-size: 14px; + line-height: 1.4; + transition: all 0.2s ease; + animation: messageAppear 0.3s ease-out; + + &:hover { + background: #e5e7eb; + } + + &.is-selected { + background: #fff; + border-color: #4f46e5; + box-shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1); + } + } + + &__actions { + display: flex; + justify-content: space-between; + align-items: center; + margin-top: 16px; + padding-top: 16px; + border-top: 1px solid #e5e7eb; + + .components-button { + height: 40px; + padding: 8px 16px; + border-radius: 20px; + font-size: 14px; + + &.is-secondary { + color: #4b5563; + } + + &.is-primary { + background: #4f46e5; + + &:hover { + background: #4338ca; + } + } + } + } + + &__completion { + display: flex; + justify-content: flex-end; + padding-top: 16px; + border-top: 1px solid #e5e7eb; + + .components-button { + height: 40px; + padding: 8px 24px; + border-radius: 20px; + font-size: 14px; + background: #4f46e5; + + &:hover { + background: #4338ca; + } + } + } +} + +@keyframes messageAppear { + from { + opacity: 0; + transform: translateY(10px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +// Keep this around for magic: +// @keyframes blink { +// 30% { cy: 30; } +// 50% { cy: 25; fill: lightgrey } +// 70% { cy: 30; } +// } +// .dot { +// animation: 1s blink linear infinite; +// fill: grey; +// } +// .dot:nth-child(2) { animation-delay: 150ms } +// .dot:nth-child(3) { animation-delay: 250ms } + +// .loader { +// background-color: #f1f1f1; +// color: grey; +// } +// +// +// +// +// From 19b258a9602e83c903e2f2c67fb04e5594804034 Mon Sep 17 00:00:00 2001 From: Christian Gastrell Date: Wed, 1 Jan 2025 14:21:09 -0300 Subject: [PATCH 02/29] changelog --- .../plugins/jetpack/changelog/add-jetpack-seo-assistant-poc | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 projects/plugins/jetpack/changelog/add-jetpack-seo-assistant-poc diff --git a/projects/plugins/jetpack/changelog/add-jetpack-seo-assistant-poc b/projects/plugins/jetpack/changelog/add-jetpack-seo-assistant-poc new file mode 100644 index 0000000000000..9bda40718c680 --- /dev/null +++ b/projects/plugins/jetpack/changelog/add-jetpack-seo-assistant-poc @@ -0,0 +1,4 @@ +Significance: minor +Type: other + +Jetpack AI: add PoC for SEO assistant, hardcoded and no actionables yet From 75226f24a6a00421bd0d73b2819abfe7d106a4d2 Mon Sep 17 00:00:00 2001 From: Christian Gastrell Date: Wed, 1 Jan 2025 16:26:28 -0300 Subject: [PATCH 03/29] interpolate better those br --- .../ai-assistant-plugin/components/seo-assistant/index.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/seo-assistant/index.tsx b/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/seo-assistant/index.tsx index 5794408e8ac26..db8de0f60ba91 100644 --- a/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/seo-assistant/index.tsx +++ b/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/seo-assistant/index.tsx @@ -233,12 +233,12 @@ export default function SeoAssistant( { busy, disabled, onStep }: SeoAssistantPr messages: [ __( "Here's your updated checklist:", 'jetpack' ), createInterpolateElement( - __( '✅ Keywords
✅ Title
✅ Meta description', 'jetpack' ), + __( '✅ Keywords
✅ Title
✅ Meta description', 'jetpack' ), { br:
} ), createInterpolateElement( __( - 'SEO optimization complete! 🎉
Your blog post is now search-engine friendly.
nHappy blogging! 😊', + 'SEO optimization complete! 🎉
Your blog post is now search-engine friendly.
Happy blogging! 😊', 'jetpack' ), { br:
} From 6e9a428a763ebb836e590b8866b0fde7bb0cc5bb Mon Sep 17 00:00:00 2001 From: Christian Gastrell Date: Thu, 2 Jan 2025 17:51:47 -0300 Subject: [PATCH 04/29] add suspenseful typing effect for async processes --- .../components/seo-assistant/index.tsx | 17 ++++++++- .../components/seo-assistant/style.scss | 37 ++++++++----------- 2 files changed, 32 insertions(+), 22 deletions(-) diff --git a/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/seo-assistant/index.tsx b/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/seo-assistant/index.tsx index db8de0f60ba91..1d1a8b0277395 100644 --- a/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/seo-assistant/index.tsx +++ b/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/seo-assistant/index.tsx @@ -1,4 +1,4 @@ -import { Button, TextControl } from '@wordpress/components'; +import { Button, TextControl, SVG, Circle } from '@wordpress/components'; import { useState, useCallback, @@ -60,6 +60,16 @@ interface SeoAssistantProps { const debug = debugFactory( 'jetpack-ai:seo-assistant' ); +const TypingMessage = () => { + return ( + + + + + + ); +}; + export default function SeoAssistant( { busy, disabled, onStep }: SeoAssistantProps ) { const [ isOpen, setIsOpen ] = useState( false ); const [ currentStep, setCurrentStep ] = useState( 0 ); @@ -106,6 +116,11 @@ export default function SeoAssistant( { busy, disabled, onStep }: SeoAssistantPr ] ); }; + /* Removes last message */ + const removeLastMessage = () => { + setMessages( prev => prev.slice( 0, -1 ) ); + }; + const handleKeywordsSubmit = useCallback( ( value: string ) => { setKeywords( value ); diff --git a/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/seo-assistant/style.scss b/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/seo-assistant/style.scss index 55bf0bae0a89d..5bb12113e5fe3 100644 --- a/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/seo-assistant/style.scss +++ b/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/seo-assistant/style.scss @@ -206,24 +206,19 @@ } // Keep this around for magic: -// @keyframes blink { -// 30% { cy: 30; } -// 50% { cy: 25; fill: lightgrey } -// 70% { cy: 30; } -// } -// .dot { -// animation: 1s blink linear infinite; -// fill: grey; -// } -// .dot:nth-child(2) { animation-delay: 150ms } -// .dot:nth-child(3) { animation-delay: 250ms } - -// .loader { -// background-color: #f1f1f1; -// color: grey; -// } -// -// -// -// -// +@keyframes typing-blink { + 30% { cy: 30; } + 50% { cy: 25; fill: lightgrey } + 70% { cy: 30; } +} +.typing-dot { + animation: 1s typing-blink linear infinite; + fill: grey; +} +.typing-dot:nth-child(2) { animation-delay: 150ms } +.typing-dot:nth-child(3) { animation-delay: 250ms } + +.typing-loader { + // background-color: #f1f1f1; + color: grey; +} From 7f1b55dcbb7e034e8177a854105fb9deb1ca00e9 Mon Sep 17 00:00:00 2001 From: Christian Gastrell Date: Thu, 2 Jan 2025 18:06:37 -0300 Subject: [PATCH 05/29] add more handlers and options for steps --- .../ai-assistant-plugin/components/seo-assistant/index.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/seo-assistant/index.tsx b/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/seo-assistant/index.tsx index 1d1a8b0277395..7132c6ac41e57 100644 --- a/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/seo-assistant/index.tsx +++ b/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/seo-assistant/index.tsx @@ -30,6 +30,7 @@ interface BaseStep { title: string; messages: string[] | React.ReactNode[]; type: StepType; + onStart?: () => void; } interface InputStep extends BaseStep { @@ -43,7 +44,9 @@ interface OptionsStep extends BaseStep { options: Option[]; onSelect: ( option: Option ) => void; onSubmit?: () => void; - onRegenerate?: () => void; + submitCtaLabel?: string; + onRetry?: () => void; + onRetryCtaLabel?: string; } interface CompletionStep extends BaseStep { From 72004d70c1892079ebe8a9408f827fc1796621ee Mon Sep 17 00:00:00 2001 From: Christian Gastrell Date: Thu, 2 Jan 2025 18:07:35 -0300 Subject: [PATCH 06/29] use empty initial states --- .../components/seo-assistant/index.tsx | 19 ++----------------- 1 file changed, 2 insertions(+), 17 deletions(-) diff --git a/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/seo-assistant/index.tsx b/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/seo-assistant/index.tsx index 7132c6ac41e57..d27177ce55a9b 100644 --- a/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/seo-assistant/index.tsx +++ b/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/seo-assistant/index.tsx @@ -81,24 +81,9 @@ export default function SeoAssistant( { busy, disabled, onStep }: SeoAssistantPr const [ selectedMetaDescription, setSelectedMetaDescription ] = useState< string >(); const [ messages, setMessages ] = useState< Message[] >( [] ); const messagesEndRef = useRef< HTMLDivElement >( null ); - const [ titleOptions, setTitleOptions ] = useState< Option[] >( [ - { - id: '1', - content: 'A Photo Gallery for Gardening Enthusiasths: Flora Guide', - }, - { - id: '2', - content: 'Flora Guide: Beautiful Photos of Flowers and Plants for Gardening Enthusiasts', - }, - ] ); + const [ titleOptions, setTitleOptions ] = useState< Option[] >( [] ); - const [ metaDescriptionOptions, setMetaDescriptionOptions ] = useState< Option[] >( [ - { - id: 'meta-1', - content: - 'Explore breathtaking flower and plant photography in our Flora Guide, featuring tips and inspiration for gardening and plant enthusiasts to enhance their outdoor spaces.', - }, - ] ); + const [ metaDescriptionOptions, setMetaDescriptionOptions ] = useState< Option[] >( [] ); const scrollToBottom = () => { messagesEndRef.current?.scrollIntoView( { behavior: 'smooth' } ); From c8e16b826f850411baa87f7147b5a8c318b2380f Mon Sep 17 00:00:00 2001 From: Christian Gastrell Date: Thu, 2 Jan 2025 18:12:23 -0300 Subject: [PATCH 07/29] fix keywords confirmation message to handle only 1 keyword --- .../ai-assistant-plugin/components/seo-assistant/index.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/seo-assistant/index.tsx b/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/seo-assistant/index.tsx index d27177ce55a9b..f35fb0928f646 100644 --- a/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/seo-assistant/index.tsx +++ b/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/seo-assistant/index.tsx @@ -117,8 +117,11 @@ export default function SeoAssistant( { busy, disabled, onStep }: SeoAssistantPr .split( ',' ) .map( k => k.trim() ) .reduce( ( acc, curr, i, arr ) => { + if ( arr.length === 1 ) { + return curr; + } if ( i === arr.length - 1 ) { - return `${ acc } & ${ curr }`; + return `${ acc }
& ${ curr }`; } return i === 0 ? curr : `${ acc }, ${ curr }`; }, '' ); From 0cd4673a967cc37decad61e70b8e7f95c1b08d30 Mon Sep 17 00:00:00 2001 From: Christian Gastrell Date: Thu, 2 Jan 2025 18:14:38 -0300 Subject: [PATCH 08/29] introduce async functions for generation/regeneration. Use new step props for labels and triggering processes --- .../components/seo-assistant/index.tsx | 128 ++++++++++++++++-- 1 file changed, 113 insertions(+), 15 deletions(-) diff --git a/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/seo-assistant/index.tsx b/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/seo-assistant/index.tsx index f35fb0928f646..d3503a50baf81 100644 --- a/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/seo-assistant/index.tsx +++ b/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/seo-assistant/index.tsx @@ -150,9 +150,60 @@ export default function SeoAssistant( { busy, disabled, onStep }: SeoAssistantPr ); }, [] ); - const handleTitleRegenerate = useCallback( () => { + const handleTitleGenerate = useCallback( async () => { + let newTitles; + // we only generate if options are empty + if ( titleOptions.length === 0 ) { + debug( 'Generating titles...' ); + addMessage( ); + newTitles = await new Promise( resolve => + setTimeout( + () => + resolve( [ + { + id: '1', + content: 'A Photo Gallery for Gardening Enthusiasths: Flora Guide', + }, + { + id: '2', + content: + 'Flora Guide: Beautiful Photos of Flowers and Plants for Gardening Enthusiasts', + }, + ] ), + 2000 + ) + ); + removeLastMessage(); + } + addMessage( 'Here are two suggestions based on your keywords. Select the one you prefer:' ); + setTitleOptions( newTitles || titleOptions ); + }, [ titleOptions ] ); + + const handleTitleRegenerate = useCallback( async () => { // This would typically be an async call to generate new titles debug( 'Regenerating titles...' ); + setTitleOptions( [] ); + addMessage( ); + const newTitles = await new Promise< Array< Option > >( resolve => + setTimeout( + () => + resolve( [ + { + id: '1', + content: 'A Photo Gallery for Gardening Enthusiasths: Flora Guide', + }, + { + id: '2', + content: + 'Flora Guide: Beautiful Photos of Flowers and Plants for Gardening Enthusiasts', + }, + ] ), + 2000 + ) + ); + removeLastMessage(); + addMessage( 'Here are two new suggestions based on your keywords. Select the one you prefer:' ); + setTitleOptions( newTitles ); }, [] ); const handleTitleSubmit = useCallback( () => { @@ -181,6 +232,53 @@ export default function SeoAssistant( { busy, disabled, onStep }: SeoAssistantPr } }, [ selectedMetaDescription, onStep ] ); + const handleMetaDescriptionGenerate = useCallback( async () => { + let newMetaDescriptions; + // we only generate if options are empty + if ( metaDescriptionOptions.length === 0 ) { + debug( 'Generating titles...' ); + addMessage( ); + newMetaDescriptions = await new Promise( resolve => + setTimeout( + () => + resolve( [ + { + id: 'meta-1', + content: + 'Explore breathtaking flower and plant photography in our Flora Guide, featuring tips and inspiration for gardening and plant enthusiasts to enhance their outdoor spaces.', + }, + ] ), + 2000 + ) + ); + removeLastMessage(); + } + addMessage( "Here's a suggestion:" ); + setMetaDescriptionOptions( newMetaDescriptions || metaDescriptionOptions ); + }, [ metaDescriptionOptions ] ); + + const handleMetaDescriptionRegenerate = useCallback( async () => { + debug( 'Generating new meta description...' ); + setMetaDescriptionOptions( [] ); + addMessage( ); + const newMetaDescription = await new Promise< Array< Option > >( resolve => + setTimeout( + () => + resolve( [ + { + id: 'meta-1', + content: + 'Explore breathtaking flower and plant photography in our Flora Guide, featuring tips and inspiration for gardening and plant enthusiasts to enhance their outdoor spaces.', + }, + ] ), + 2000 + ) + ); + removeLastMessage(); + addMessage( "Here's a new suggestion:" ); + setMetaDescriptionOptions( newMetaDescription ); + }, [] ); + const handleDone = useCallback( () => { setIsOpen( false ); setCurrentStep( 0 ); @@ -209,29 +307,28 @@ export default function SeoAssistant( { busy, disabled, onStep }: SeoAssistantPr { id: 'title', title: __( 'Optimise Title', 'jetpack' ), - messages: [ - __( - "Let's optimise your title. Here are two suggestions based on your keywords. Select the one you prefer:", - 'jetpack' - ), - ], + messages: [ __( "Let's optimise your title.", 'jetpack' ) ], type: 'options', options: titleOptions, onSelect: handleTitleSelect, onSubmit: handleTitleSubmit, - onRegenerate: handleTitleRegenerate, + submitCtaLabel: __( 'Insert', 'jetpack' ), + onRetry: handleTitleRegenerate, + onRetryCtaLabel: __( 'Regenerate', 'jetpack' ), + onStart: handleTitleGenerate, }, { id: 'meta', title: __( 'Add meta description', 'jetpack' ), - messages: [ - __( "Now, let's optimize your meta description. Here's a suggestion:", 'jetpack' ), - ], + messages: [ __( "Now, let's optimize your meta description.", 'jetpack' ) ], type: 'options', options: metaDescriptionOptions, onSelect: handleMetaDescriptionSelect, onSubmit: handleMetaDescriptionSubmit, - onRegenerate: handleTitleRegenerate, // Reuse the same handler for now + submitCtaLabel: __( 'Insert', 'jetpack' ), + onRetry: handleMetaDescriptionRegenerate, // Reuse the same handler for now + onRetryCtaLabel: __( 'Regenerate', 'jetpack' ), + onStart: handleMetaDescriptionGenerate, // Reuse the same handler for now }, { id: 'completion', @@ -269,6 +366,7 @@ export default function SeoAssistant( { busy, disabled, onStep }: SeoAssistantPr setCurrentStep( currentStep + 1 ); // Add next step messages steps[ currentStep + 1 ].messages.forEach( message => addMessage( message ) ); + steps[ currentStep + 1 ].onStart?.(); } }; @@ -342,8 +440,8 @@ export default function SeoAssistant( { busy, disabled, onStep }: SeoAssistantPr ) ) }
- { selectedOption && ( ) }
From 463766b2fdde78210385351fc4f920c13daa5d97 Mon Sep 17 00:00:00 2001 From: Christian Gastrell Date: Mon, 6 Jan 2025 15:44:18 -0300 Subject: [PATCH 09/29] fix initial task interpolated elements --- .../ai-assistant-plugin/components/seo-assistant/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/seo-assistant/index.tsx b/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/seo-assistant/index.tsx index d3503a50baf81..eeb2d74d79016 100644 --- a/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/seo-assistant/index.tsx +++ b/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/seo-assistant/index.tsx @@ -293,7 +293,7 @@ export default function SeoAssistant( { busy, disabled, onStep }: SeoAssistantPr __( "Hi there! 👋 Let's optimise your blog post for SEO.", 'jetpack' ), createInterpolateElement( __( - "Here's what we can improve:
1. Keywords
2. Title
3. Meta description", + "Here's what we can improve:
1. Keywords
2. Title
3. Meta description", 'jetpack' ), { br:
} From 2cb4beea9d562c958775f59ae7afef751867b717 Mon Sep 17 00:00:00 2001 From: Christian Gastrell Date: Mon, 6 Jan 2025 15:44:57 -0300 Subject: [PATCH 10/29] do not render the assistant is the post type is not viewable --- .../components/seo-assistant/index.tsx | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/seo-assistant/index.tsx b/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/seo-assistant/index.tsx index eeb2d74d79016..4a2ef9104c105 100644 --- a/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/seo-assistant/index.tsx +++ b/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/seo-assistant/index.tsx @@ -1,4 +1,7 @@ import { Button, TextControl, SVG, Circle } from '@wordpress/components'; +import { store as coreStore } from '@wordpress/core-data'; +import { useSelect } from '@wordpress/data'; +import { store as editorStore } from '@wordpress/editor'; import { useState, useCallback, @@ -10,6 +13,7 @@ import { __, sprintf } from '@wordpress/i18n'; import clsx from 'clsx'; import debugFactory from 'debug'; import './style.scss'; +import { CoreSelect } from '../ai-assistant-plugin-sidebar/types'; type StepType = 'input' | 'options' | 'completion'; @@ -82,6 +86,16 @@ export default function SeoAssistant( { busy, disabled, onStep }: SeoAssistantPr const [ messages, setMessages ] = useState< Message[] >( [] ); const messagesEndRef = useRef< HTMLDivElement >( null ); const [ titleOptions, setTitleOptions ] = useState< Option[] >( [] ); + // const postContent = usePostContent(); + // const { isLoadingModules, isChangingStatus, isModuleActive, changeStatus } = useModuleStatus( 'seo-tools' ); + const isViewable = useSelect( select => { + const postTypeName = select( editorStore ).getCurrentPostType(); + const postTypeObject = ( select( coreStore ) as unknown as CoreSelect ).getPostType( + postTypeName + ); + + return postTypeObject?.viewable; + }, [] ); const [ metaDescriptionOptions, setMetaDescriptionOptions ] = useState< Option[] >( [] ); @@ -384,6 +398,11 @@ export default function SeoAssistant( { busy, disabled, onStep }: SeoAssistantPr setMessages( [] ); }; + // If the post type is not viewable, do not render my plugin. + if ( ! isViewable ) { + return null; + } + if ( ! isOpen ) { return (
From d1317a2284fbb60591f9b15ae3f5a4d45c4ca09e Mon Sep 17 00:00:00 2001 From: Christian Gastrell Date: Mon, 6 Jan 2025 19:29:56 -0300 Subject: [PATCH 11/29] reorganize code, add module status management and CTA states --- .../ai-assistant-plugin-sidebar/index.tsx | 12 +- .../components/seo-assistant/index.tsx | 118 +++++++++--------- 2 files changed, 66 insertions(+), 64 deletions(-) diff --git a/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/ai-assistant-plugin-sidebar/index.tsx b/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/ai-assistant-plugin-sidebar/index.tsx index 66878a350339b..304e302a39829 100644 --- a/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/ai-assistant-plugin-sidebar/index.tsx +++ b/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/ai-assistant-plugin-sidebar/index.tsx @@ -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' ); @@ -89,7 +97,7 @@ const JetpackAndSettingsContent = ( { ) } - { isSeoAssistantEnabled && ( + { isSeoAssistantEnabled && isViewable && ( { __( 'SEO', 'jetpack' ) } - + ) } diff --git a/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/seo-assistant/index.tsx b/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/seo-assistant/index.tsx index 4a2ef9104c105..6bda862f650d7 100644 --- a/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/seo-assistant/index.tsx +++ b/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/seo-assistant/index.tsx @@ -1,7 +1,5 @@ +import { useModuleStatus } from '@automattic/jetpack-shared-extension-utils'; import { Button, TextControl, SVG, Circle } from '@wordpress/components'; -import { store as coreStore } from '@wordpress/core-data'; -import { useSelect } from '@wordpress/data'; -import { store as editorStore } from '@wordpress/editor'; import { useState, useCallback, @@ -12,8 +10,9 @@ import { import { __, sprintf } from '@wordpress/i18n'; import clsx from 'clsx'; import debugFactory from 'debug'; +import { SeoPlaceholder } from '../../../../plugins/seo/components/placeholder'; +import usePostContent from '../../hooks/use-post-content'; import './style.scss'; -import { CoreSelect } from '../ai-assistant-plugin-sidebar/types'; type StepType = 'input' | 'options' | 'completion'; @@ -77,7 +76,7 @@ const TypingMessage = () => { ); }; -export default function SeoAssistant( { busy, disabled, onStep }: SeoAssistantProps ) { +export default function SeoAssistant( { disabled, onStep }: SeoAssistantProps ) { const [ isOpen, setIsOpen ] = useState( false ); const [ currentStep, setCurrentStep ] = useState( 0 ); const [ keywords, setKeywords ] = useState( '' ); @@ -86,16 +85,9 @@ export default function SeoAssistant( { busy, disabled, onStep }: SeoAssistantPr const [ messages, setMessages ] = useState< Message[] >( [] ); const messagesEndRef = useRef< HTMLDivElement >( null ); const [ titleOptions, setTitleOptions ] = useState< Option[] >( [] ); - // const postContent = usePostContent(); - // const { isLoadingModules, isChangingStatus, isModuleActive, changeStatus } = useModuleStatus( 'seo-tools' ); - const isViewable = useSelect( select => { - const postTypeName = select( editorStore ).getCurrentPostType(); - const postTypeObject = ( select( coreStore ) as unknown as CoreSelect ).getPostType( - postTypeName - ); - - return postTypeObject?.viewable; - }, [] ); + const postContent = usePostContent(); + const { isLoadingModules, isChangingStatus, isModuleActive, changeStatus } = + useModuleStatus( 'seo-tools' ); const [ metaDescriptionOptions, setMetaDescriptionOptions ] = useState< Option[] >( [] ); @@ -398,27 +390,6 @@ export default function SeoAssistant( { busy, disabled, onStep }: SeoAssistantPr setMessages( [] ); }; - // If the post type is not viewable, do not render my plugin. - if ( ! isViewable ) { - return null; - } - - if ( ! isOpen ) { - return ( -
-

{ __( 'Improve post engagement.', 'jetpack' ) }

- -
- ); - } - const renderCurrentInput = () => { if ( currentStepData.type === 'input' ) { return ( @@ -490,36 +461,59 @@ export default function SeoAssistant( { busy, disabled, onStep }: SeoAssistantPr return null; }; + debug( isModuleActive, isLoadingModules ); return ( -
-
- -

{ currentStepData.title }

- -
- -
-
- { messages.map( message => ( -
- { message.content } +
+

{ __( 'Improve post engagement.', 'jetpack' ) }

+ { ( isModuleActive || isLoadingModules ) && ( + + ) } + { ! isModuleActive && ! isLoadingModules && ( + + ) } + { isOpen && ( +
+
+ +

{ currentStepData.title }

+ +
+ +
+
+ { messages.map( message => ( +
+ { message.content } +
+ ) ) } +
- ) ) } -
-
-
{ renderCurrentInput() }
-
+
{ renderCurrentInput() }
+
+
+ ) }
); } From 917779402eda561698ae088ac53c049e6ab2fe36 Mon Sep 17 00:00:00 2001 From: Christian Gastrell Date: Wed, 8 Jan 2025 18:56:31 -0300 Subject: [PATCH 12/29] move SEO assistant CTA to Jetpack sidebar under SEO panel --- .../ai-assistant-plugin-sidebar/index.tsx | 24 +++++++++---------- .../jetpack/extensions/plugins/seo/index.js | 15 ++++++++++++ 2 files changed, 27 insertions(+), 12 deletions(-) diff --git a/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/ai-assistant-plugin-sidebar/index.tsx b/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/ai-assistant-plugin-sidebar/index.tsx index 304e302a39829..221a2f509c3b9 100644 --- a/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/ai-assistant-plugin-sidebar/index.tsx +++ b/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/ai-assistant-plugin-sidebar/index.tsx @@ -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 { @@ -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, @@ -72,14 +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 - ); + // const isViewable = useSelect( select => { + // const postTypeName = select( editorStore ).getCurrentPostType(); + // const postTypeObject = ( select( coreStore ) as unknown as CoreSelect ).getPostType( + // postTypeName + // ); - return postTypeObject?.viewable; - }, [] ); + // return postTypeObject?.viewable; + // }, [] ); const currentTitleOptimizationSectionLabel = __( 'Optimize Publishing', 'jetpack' ); const SEOTitleOptimizationSectionLabel = __( 'Optimize Title', 'jetpack' ); @@ -97,7 +97,7 @@ const JetpackAndSettingsContent = ( { ) } - { isSeoAssistantEnabled && isViewable && ( + { /* { isSeoAssistantEnabled && isViewable && ( - ) } + ) } */ } { canWriteBriefBeEnabled() && isBreveAvailable && ( diff --git a/projects/plugins/jetpack/extensions/plugins/seo/index.js b/projects/plugins/jetpack/extensions/plugins/seo/index.js index 4ebdeb0185543..a3618e5a60fa0 100644 --- a/projects/plugins/jetpack/extensions/plugins/seo/index.js +++ b/projects/plugins/jetpack/extensions/plugins/seo/index.js @@ -3,6 +3,7 @@ import { useModuleStatus, isSimpleSite, isAtomicSite, + getJetpackExtensionAvailability, getRequiredPlan, } from '@automattic/jetpack-shared-extension-utils'; import { PanelBody, PanelRow } from '@wordpress/components'; @@ -12,7 +13,9 @@ import { PluginPrePublishPanel } from '@wordpress/edit-post'; import { store as editorStore } from '@wordpress/editor'; import { Fragment } from '@wordpress/element'; import { __ } from '@wordpress/i18n'; +import { isBetaExtension } from '../../editor'; import JetpackPluginSidebar from '../../shared/jetpack-plugin-sidebar'; +import SeoAssistant from '../ai-assistant-plugin/components/seo-assistant'; import { SeoPlaceholder } from './components/placeholder'; import { SeoSkeletonLoader } from './components/skeleton-loader'; import UpsellNotice from './components/upsell'; @@ -24,6 +27,9 @@ import './editor.scss'; export const name = 'seo'; +const isSeoAssistantEnabled = + getJetpackExtensionAvailability( 'ai-seo-assistant' )?.available === true; + const Seo = () => { const { isLoadingModules, isChangingStatus, isModuleActive, changeStatus } = useModuleStatus( 'seo-tools' ); @@ -95,6 +101,15 @@ const Seo = () => { + { isSeoAssistantEnabled && isViewable && ( + + + + ) } From 4f9a41bae70cc09aeb32e31e6afc77eb47de9a8a Mon Sep 17 00:00:00 2001 From: Christian Gastrell Date: Wed, 8 Jan 2025 18:57:03 -0300 Subject: [PATCH 13/29] refactor some behaviors and adjust styles towards designs --- .../components/seo-assistant/big-sky-icon.svg | 4 + .../components/seo-assistant/index.tsx | 217 ++++++++++++------ .../components/seo-assistant/style.scss | 130 +++++++---- 3 files changed, 239 insertions(+), 112 deletions(-) create mode 100644 projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/seo-assistant/big-sky-icon.svg diff --git a/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/seo-assistant/big-sky-icon.svg b/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/seo-assistant/big-sky-icon.svg new file mode 100644 index 0000000000000..9ae8660e1f429 --- /dev/null +++ b/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/seo-assistant/big-sky-icon.svg @@ -0,0 +1,4 @@ + + + + diff --git a/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/seo-assistant/index.tsx b/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/seo-assistant/index.tsx index 6bda862f650d7..51a2b0f86b80f 100644 --- a/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/seo-assistant/index.tsx +++ b/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/seo-assistant/index.tsx @@ -1,5 +1,5 @@ import { useModuleStatus } from '@automattic/jetpack-shared-extension-utils'; -import { Button, TextControl, SVG, Circle } from '@wordpress/components'; +import { Button, TextControl, SVG, Circle, Icon } from '@wordpress/components'; import { useState, useCallback, @@ -8,11 +8,13 @@ import { createInterpolateElement, } from '@wordpress/element'; import { __, sprintf } from '@wordpress/i18n'; +import { arrowRight } from '@wordpress/icons'; import clsx from 'clsx'; import debugFactory from 'debug'; import { SeoPlaceholder } from '../../../../plugins/seo/components/placeholder'; import usePostContent from '../../hooks/use-post-content'; import './style.scss'; +import bigSkyIcon from './big-sky-icon.svg'; type StepType = 'input' | 'options' | 'completion'; @@ -20,6 +22,7 @@ interface Message { id: string; content: string | React.ReactNode; isUser?: boolean; + showIcon?: boolean; } interface Option { @@ -31,7 +34,7 @@ interface Option { interface BaseStep { id: string; title: string; - messages: string[] | React.ReactNode[]; + messages: StepMessage[]; type: StepType; onStart?: () => void; } @@ -76,6 +79,11 @@ const TypingMessage = () => { ); }; +interface StepMessage { + content: string | React.ReactNode; + showIcon?: boolean; +} + export default function SeoAssistant( { disabled, onStep }: SeoAssistantProps ) { const [ isOpen, setIsOpen ] = useState( false ); const [ currentStep, setCurrentStep ] = useState( 0 ); @@ -99,13 +107,14 @@ export default function SeoAssistant( { disabled, onStep }: SeoAssistantProps ) scrollToBottom(); }, [ messages ] ); - const addMessage = ( content: string | React.ReactNode, isUser = false ) => { + const addMessage = ( content: string | React.ReactNode, isUser = false, showIcon = ! isUser ) => { setMessages( prev => [ ...prev, { id: `message-${ prev.length }`, content, isUser, + showIcon, }, ] ); }; @@ -296,15 +305,30 @@ export default function SeoAssistant( { disabled, onStep }: SeoAssistantProps ) id: 'keywords', title: __( 'Optimise for SEO', 'jetpack' ), messages: [ - __( "Hi there! 👋 Let's optimise your blog post for SEO.", 'jetpack' ), - createInterpolateElement( - __( - "Here's what we can improve:
1. Keywords
2. Title
3. Meta description", + { + content: createInterpolateElement( + __( "Hi there! 👋 Let's optimise your blog post for SEO.", 'jetpack' ), + { b: } + ), + showIcon: true, + }, + { + content: createInterpolateElement( + __( + "Here's what we can improve:
1. Keywords
2. Title
3. Meta description", + 'jetpack' + ), + { br:
} + ), + showIcon: false, + }, + { + content: __( + 'To start, please enter 1–3 focus keywords that describe your blog post.', 'jetpack' ), - { br:
} - ), - __( 'To start, please enter 1–3 focus keywords that describe your blog post.', 'jetpack' ), + showIcon: true, + }, ], type: 'input', placeholder: __( 'Photography, plants', 'jetpack' ), @@ -313,7 +337,12 @@ export default function SeoAssistant( { disabled, onStep }: SeoAssistantProps ) { id: 'title', title: __( 'Optimise Title', 'jetpack' ), - messages: [ __( "Let's optimise your title.", 'jetpack' ) ], + messages: [ + { + content: __( "Let's optimise your title.", 'jetpack' ), + showIcon: true, + }, + ], type: 'options', options: titleOptions, onSelect: handleTitleSelect, @@ -326,32 +355,50 @@ export default function SeoAssistant( { disabled, onStep }: SeoAssistantProps ) { id: 'meta', title: __( 'Add meta description', 'jetpack' ), - messages: [ __( "Now, let's optimize your meta description.", 'jetpack' ) ], + messages: [ + { + content: __( "Now, let's optimize your meta description.", 'jetpack' ), + showIcon: true, + }, + ], type: 'options', options: metaDescriptionOptions, onSelect: handleMetaDescriptionSelect, onSubmit: handleMetaDescriptionSubmit, submitCtaLabel: __( 'Insert', 'jetpack' ), - onRetry: handleMetaDescriptionRegenerate, // Reuse the same handler for now + onRetry: handleMetaDescriptionRegenerate, onRetryCtaLabel: __( 'Regenerate', 'jetpack' ), - onStart: handleMetaDescriptionGenerate, // Reuse the same handler for now + onStart: handleMetaDescriptionGenerate, }, { id: 'completion', title: __( 'Your post is SEO-ready', 'jetpack' ), messages: [ - __( "Here's your updated checklist:", 'jetpack' ), - createInterpolateElement( - __( '✅ Keywords
✅ Title
✅ Meta description', 'jetpack' ), - { br:
} - ), - createInterpolateElement( - __( - 'SEO optimization complete! 🎉
Your blog post is now search-engine friendly.
Happy blogging! 😊', - 'jetpack' + { + content: __( "Here's your updated checklist:", 'jetpack' ), + showIcon: true, + }, + { + content: createInterpolateElement( + __( '✅ Keywords
✅ Title
✅ Meta description', 'jetpack' ), + { br:
} + ), + showIcon: false, + }, + { + content: createInterpolateElement( + __( + 'SEO optimization complete! 🎉
Your blog post is now search-engine friendly.', + 'jetpack' + ), + { br:
} ), - { br:
} - ), + showIcon: true, + }, + { + content: __( 'Happy blogging! 😊', 'jetpack' ), + showIcon: false, + }, ], type: 'completion', }, @@ -362,7 +409,9 @@ export default function SeoAssistant( { disabled, onStep }: SeoAssistantProps ) useEffect( () => { if ( isOpen && messages.length === 0 ) { // Initialize with first step messages - currentStepData.messages.forEach( message => addMessage( message ) ); + currentStepData.messages.forEach( message => + addMessage( message.content, false, message.showIcon ) + ); } }, [ isOpen, currentStepData.messages, messages ] ); @@ -371,7 +420,9 @@ export default function SeoAssistant( { disabled, onStep }: SeoAssistantProps ) debug( 'moving to ' + ( currentStep + 1 ), steps[ currentStep + 1 ] ); setCurrentStep( currentStep + 1 ); // Add next step messages - steps[ currentStep + 1 ].messages.forEach( message => addMessage( message ) ); + steps[ currentStep + 1 ].messages.forEach( message => + addMessage( message.content, false, message.showIcon ) + ); steps[ currentStep + 1 ].onStart?.(); } }; @@ -380,7 +431,9 @@ export default function SeoAssistant( { disabled, onStep }: SeoAssistantProps ) if ( currentStep > 0 ) { setCurrentStep( currentStep - 1 ); // Re-add previous step messages - steps[ currentStep - 1 ].messages.forEach( message => addMessage( message ) ); + steps[ currentStep - 1 ].messages.forEach( message => + addMessage( message.content, false, message.showIcon ) + ); } }; @@ -406,8 +459,9 @@ export default function SeoAssistant( { disabled, onStep }: SeoAssistantProps ) currentStepData.onSubmit?.( keywords ); handleNext(); } } + size="small" > - { __( '↑', 'jetpack' ) } + ↑
); @@ -415,36 +469,23 @@ export default function SeoAssistant( { disabled, onStep }: SeoAssistantProps ) if ( currentStepData.type === 'options' ) { const selectedOption = currentStepData.options.find( opt => opt.selected ); - return ( -
- { currentStepData.options.map( option => ( - - ) ) } -
- - { selectedOption && ( - - ) } -
+
+ + +
); } @@ -461,7 +502,51 @@ export default function SeoAssistant( { disabled, onStep }: SeoAssistantProps ) return null; }; - debug( isModuleActive, isLoadingModules ); + + const renderMessages = () => { + return messages.map( message => ( +
+
+ { message.showIcon && ( + { + ) } +
+
{ message.content }
+
+ ) ); + }; + + const renderOptions = () => { + if ( currentStepData.type !== 'options' || ! currentStepData.options.length ) { + return null; + } + + return ( +
+
+
+
+ { currentStepData.options.map( option => ( + + ) ) } +
+
+
+ ); + }; return (
@@ -473,6 +558,8 @@ export default function SeoAssistant( { disabled, onStep }: SeoAssistantProps ) disabled={ isLoadingModules || isOpen || ! postContent.trim?.() || disabled } isBusy={ isLoadingModules || isOpen } > + { +   { __( 'SEO Assistant', 'jetpack' ) } ) } @@ -497,16 +584,8 @@ export default function SeoAssistant( { disabled, onStep }: SeoAssistantProps )
- { messages.map( message => ( -
- { message.content } -
- ) ) } + { renderMessages() } + { renderOptions() }
diff --git a/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/seo-assistant/style.scss b/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/seo-assistant/style.scss index 5bb12113e5fe3..c1db241e30f3b 100644 --- a/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/seo-assistant/style.scss +++ b/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/seo-assistant/style.scss @@ -3,12 +3,11 @@ bottom: 32px; left: 50%; transform: translateX(-50%); - width: 90%; - max-width: 600px; - height: 600px; + width: 384px; + height: 434px; background: white; - border-radius: 16px; - box-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1); + border-radius: 24px; + outline: 0.5px solid var( --jp-gray-5 ); z-index: 1000; display: flex; flex-direction: column; @@ -19,7 +18,7 @@ align-items: center; justify-content: space-between; padding: 16px; - border-bottom: 1px solid #e5e7eb; + // border-bottom: 1px solid #e5e7eb; background: white; border-radius: 16px 16px 0 0; @@ -55,7 +54,7 @@ display: flex; flex-direction: column; gap: 16px; - padding: 16px; + padding: 16px 24px; overflow-y: auto; scroll-behavior: smooth; align-items: flex-start; @@ -68,46 +67,90 @@ /* Hide scrollbar for IE, Edge and Firefox */ -ms-overflow-style: none; scrollbar-width: none; + + mask-image: linear-gradient( 180deg, transparent, white 24px ); } &__message { - padding: 12px 16px; + // padding: 12px 16px; border-radius: 16px; - background: #e0e7ff; + // background: #e0e7ff; white-space: pre-line; animation: messageAppear 0.3s ease-out; - font-size: 14px; + font-size: 13px; line-height: 1.5; + display: flex; + align-items: center; + min-width: 48px; + + .seo-assistant-wizard__message-icon { + // height: 26px; + // width: 26px; + // text-align: center; + // line-height: 4; + // flex: 0 1 26px; + flex-shrink: 0; + align-self: center; + flex-basis: 26px; + + img { + vertical-align: middle; + } + } + + .seo-assistant-wizard__message-text { + padding: 8px 12px; + flex: 1 0 200px; + } &.is-user { background: #f3f4f6; align-self: flex-end; - border-bottom-right-radius: 4px; + + .seo-assistant-wizard__message-icon { + flex-basis: unset; + } } - &:not(.is-user) { - border-bottom-left-radius: 4px; - } + // &:not(.is-user) { + // border-bottom-left-radius: 4px; + // } } &__input-container { flex: 0 0 auto; padding: 16px; background: white; - border-top: 1px solid #e5e7eb; + // border-top: 1px solid #e5e7eb; border-radius: 0 0 16px 16px; + border-top: 1px solid var( --jp-gray-5, #e5e7eb ); } &__input { display: flex; gap: 8px; + border-radius: 12px; + outline: 1px solid var( --jp-gray-10, #c3c3c3 ); + align-items: center; + padding-right: 6px; + height: 44px; + + &:focus-within { + outline-width: 2px; + outline-color: var( --wp-components-color-accent, var( --wp-admin-theme-color, #007cba ) ); + } .components-base-control { flex-grow: 1; } - .components-text-control__input { - border-radius: 24px; - padding: 8px 16px; + .components-text-control__input, + .components-text-control__input:focus { + padding: 8px; + border: 0; + border-radius: 12px; + box-shadow: none; + outline: 0; + // .components-text-control__input[type=text] } } @@ -141,54 +184,55 @@ &.is-selected { background: #fff; - border-color: #4f46e5; - box-shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1); + border-color: var( --wp-components-color-accent, var( --wp-admin-theme-color, #007cba ) ); + // box-shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1); } } &__actions { display: flex; - justify-content: space-between; + justify-content: flex-end; align-items: center; - margin-top: 16px; - padding-top: 16px; - border-top: 1px solid #e5e7eb; + // margin-top: 16px; + // padding-top: 16px; + // border-top: 1px solid #e5e7eb; + gap: 16px; .components-button { - height: 40px; - padding: 8px 16px; + // height: 40px; + // padding: 8px 16px; border-radius: 20px; - font-size: 14px; + // font-size: 14px; - &.is-secondary { - color: #4b5563; - } + // &.is-secondary { + // color: #4b5563; + // } - &.is-primary { - background: #4f46e5; + // &.is-primary { + // background: #4f46e5; - &:hover { - background: #4338ca; - } - } + // &:hover { + // background: #4338ca; + // } + // } } } &__completion { display: flex; justify-content: flex-end; - padding-top: 16px; - border-top: 1px solid #e5e7eb; + // padding-top: 16px; + // border-top: 1px solid #e5e7eb; .components-button { - height: 40px; - padding: 8px 24px; + // height: 40px; + // padding: 8px 24px; border-radius: 20px; - font-size: 14px; - background: #4f46e5; + // font-size: 14px; + // background: #4f46e5; &:hover { - background: #4338ca; + // background: #4338ca; } } } From 6881445744f145248351859c6e989503292c9c5b Mon Sep 17 00:00:00 2001 From: Christian Gastrell Date: Thu, 9 Jan 2025 17:36:25 -0300 Subject: [PATCH 14/29] refactor code to split steps, improve chat flow, add first meta handler --- .../components/seo-assistant/index.tsx | 380 ++++++------------ .../components/seo-assistant/style.scss | 20 +- .../seo-assistant/use-keywords-step.tsx | 87 ++++ .../use-meta-description-step.tsx | 105 +++++ .../seo-assistant/use-title-step.tsx | 134 ++++++ 5 files changed, 451 insertions(+), 275 deletions(-) create mode 100644 projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/seo-assistant/use-keywords-step.tsx create mode 100644 projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/seo-assistant/use-meta-description-step.tsx create mode 100644 projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/seo-assistant/use-title-step.tsx diff --git a/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/seo-assistant/index.tsx b/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/seo-assistant/index.tsx index 51a2b0f86b80f..8ba803a2e7081 100644 --- a/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/seo-assistant/index.tsx +++ b/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/seo-assistant/index.tsx @@ -7,7 +7,7 @@ import { useRef, createInterpolateElement, } from '@wordpress/element'; -import { __, sprintf } from '@wordpress/i18n'; +import { __ } from '@wordpress/i18n'; import { arrowRight } from '@wordpress/icons'; import clsx from 'clsx'; import debugFactory from 'debug'; @@ -15,17 +15,22 @@ import { SeoPlaceholder } from '../../../../plugins/seo/components/placeholder'; import usePostContent from '../../hooks/use-post-content'; import './style.scss'; import bigSkyIcon from './big-sky-icon.svg'; +import { useKeywordsStep } from './use-keywords-step'; +import { useMetaDescriptionStep } from './use-meta-description-step'; +import { useTitleStep } from './use-title-step'; type StepType = 'input' | 'options' | 'completion'; -interface Message { - id: string; - content: string | React.ReactNode; +export interface Message { + id?: string; + content?: string | React.ReactNode; isUser?: boolean; showIcon?: boolean; + type?: string; + options?: Option[]; } -interface Option { +export interface Option { id: string; content: string; selected?: boolean; @@ -43,6 +48,8 @@ interface InputStep extends BaseStep { type: 'input'; placeholder: string; onSubmit: ( value: string ) => void; + // value: string; + // setValue: React.Dispatch< React.SetStateAction< string > >; } interface OptionsStep extends BaseStep { @@ -59,7 +66,7 @@ interface CompletionStep extends BaseStep { type: 'completion'; } -type Step = InputStep | OptionsStep | CompletionStep; +export type Step = InputStep | OptionsStep | CompletionStep; interface SeoAssistantProps { busy?: boolean; @@ -67,9 +74,17 @@ interface SeoAssistantProps { onStep?: ( data: { value: string | Option | null } ) => void; } +type StepHook = { + stepProps: Step; + value: string | Array< string >; + setValue: + | React.Dispatch< React.SetStateAction< string > > + | React.Dispatch< React.SetStateAction< Array< string > > >; +}; + const debug = debugFactory( 'jetpack-ai:seo-assistant' ); -const TypingMessage = () => { +export const TypingMessage = () => { return ( @@ -87,17 +102,17 @@ interface StepMessage { export default function SeoAssistant( { disabled, onStep }: SeoAssistantProps ) { const [ isOpen, setIsOpen ] = useState( false ); const [ currentStep, setCurrentStep ] = useState( 0 ); - const [ keywords, setKeywords ] = useState( '' ); - const [ selectedTitle, setSelectedTitle ] = useState< string >(); - const [ selectedMetaDescription, setSelectedMetaDescription ] = useState< string >(); + // const [ keywords, setKeywords ] = useState( '' ); + // const [ selectedTitle, setSelectedTitle ] = useState< string >(); + // const [ selectedMetaDescription, setSelectedMetaDescription ] = useState< string >(); const [ messages, setMessages ] = useState< Message[] >( [] ); const messagesEndRef = useRef< HTMLDivElement >( null ); - const [ titleOptions, setTitleOptions ] = useState< Option[] >( [] ); + // const [ titleOptions, setTitleOptions ] = useState< Option[] >( [] ); const postContent = usePostContent(); const { isLoadingModules, isChangingStatus, isModuleActive, changeStatus } = useModuleStatus( 'seo-tools' ); - const [ metaDescriptionOptions, setMetaDescriptionOptions ] = useState< Option[] >( [] ); + // const [ metaDescriptionOptions, setMetaDescriptionOptions ] = useState< Option[] >( [] ); const scrollToBottom = () => { messagesEndRef.current?.scrollIntoView( { behavior: 'smooth' } ); @@ -107,269 +122,72 @@ export default function SeoAssistant( { disabled, onStep }: SeoAssistantProps ) scrollToBottom(); }, [ messages ] ); - const addMessage = ( content: string | React.ReactNode, isUser = false, showIcon = ! isUser ) => { - setMessages( prev => [ - ...prev, - { - id: `message-${ prev.length }`, - content, - isUser, - showIcon, - }, - ] ); + const addMessage = ( message: Message | string ) => { + setMessages( prev => { + const newMessage = { + id: + typeof message === 'string' + ? `message-${ prev.length }` + : message?.id || `message-${ prev.length }`, + content: typeof message === 'string' ? message : message.content, + isUser: typeof message === 'string' ? false : message?.isUser || false, + showIcon: typeof message === 'string' ? true : message?.showIcon ?? ! message.isUser, + type: typeof message === 'string' ? null : message?.type || null, + options: typeof message === 'string' ? [] : message?.options || [], + } as Message; + return [ ...prev, newMessage ]; + } ); }; + // const editMessage = useCallback( ( messageId: string, updatedMessage: Partial< Message > ) => { + // setMessages( prev => + // prev.map( message => + // message.id === messageId + // ? { + // ...message, + // ...updatedMessage, + // } + // : message + // ) + // ); + // }, [] ); + /* Removes last message */ const removeLastMessage = () => { setMessages( prev => prev.slice( 0, -1 ) ); }; - const handleKeywordsSubmit = useCallback( - ( value: string ) => { - setKeywords( value ); - addMessage( value, true ); - const keywordlist = value - .split( ',' ) - .map( k => k.trim() ) - .reduce( ( acc, curr, i, arr ) => { - if ( arr.length === 1 ) { - return curr; - } - if ( i === arr.length - 1 ) { - return `${ acc }
& ${ curr }`; - } - return i === 0 ? curr : `${ acc }, ${ curr }`; - }, '' ); - const message = createInterpolateElement( - /* Translators: wrapped string is list of keywords user has entered */ - sprintf( __( `Got it! You're targeting %s. ✨✅`, 'jetpack' ), keywordlist ), - { - b: , - } - ); - addMessage( message ); - if ( onStep ) { - onStep( { value: value } ); - } - }, - [ onStep ] - ); - - const handleTitleSelect = useCallback( ( option: Option ) => { - setSelectedTitle( option.content ); - setTitleOptions( prev => - prev.map( opt => ( { - ...opt, - selected: opt.id === option.id, - } ) ) - ); - }, [] ); - - const handleTitleGenerate = useCallback( async () => { - let newTitles; - // we only generate if options are empty - if ( titleOptions.length === 0 ) { - debug( 'Generating titles...' ); - addMessage( ); - newTitles = await new Promise( resolve => - setTimeout( - () => - resolve( [ - { - id: '1', - content: 'A Photo Gallery for Gardening Enthusiasths: Flora Guide', - }, - { - id: '2', - content: - 'Flora Guide: Beautiful Photos of Flowers and Plants for Gardening Enthusiasts', - }, - ] ), - 2000 - ) - ); - removeLastMessage(); - } - addMessage( 'Here are two suggestions based on your keywords. Select the one you prefer:' ); - setTitleOptions( newTitles || titleOptions ); - }, [ titleOptions ] ); - - const handleTitleRegenerate = useCallback( async () => { - // This would typically be an async call to generate new titles - debug( 'Regenerating titles...' ); - setTitleOptions( [] ); - addMessage( ); - const newTitles = await new Promise< Array< Option > >( resolve => - setTimeout( - () => - resolve( [ - { - id: '1', - content: 'A Photo Gallery for Gardening Enthusiasths: Flora Guide', - }, - { - id: '2', - content: - 'Flora Guide: Beautiful Photos of Flowers and Plants for Gardening Enthusiasts', - }, - ] ), - 2000 - ) - ); - removeLastMessage(); - addMessage( 'Here are two new suggestions based on your keywords. Select the one you prefer:' ); - setTitleOptions( newTitles ); - }, [] ); - - const handleTitleSubmit = useCallback( () => { - addMessage( selectedTitle, true ); - addMessage( __( 'Title updated! ✅', 'jetpack' ) ); - if ( onStep ) { - onStep( { value: selectedTitle } ); - } - }, [ selectedTitle, onStep ] ); - - const handleMetaDescriptionSelect = useCallback( ( option: Option ) => { - setSelectedMetaDescription( option.content ); - setMetaDescriptionOptions( prev => - prev.map( opt => ( { - ...opt, - selected: opt.id === option.id, - } ) ) - ); - }, [] ); - - const handleMetaDescriptionSubmit = useCallback( () => { - addMessage( selectedMetaDescription, true ); - addMessage( __( 'Meta description updated! ✅', 'jetpack' ) ); - if ( onStep ) { - onStep( { value: selectedMetaDescription } ); - } - }, [ selectedMetaDescription, onStep ] ); - - const handleMetaDescriptionGenerate = useCallback( async () => { - let newMetaDescriptions; - // we only generate if options are empty - if ( metaDescriptionOptions.length === 0 ) { - debug( 'Generating titles...' ); - addMessage( ); - newMetaDescriptions = await new Promise( resolve => - setTimeout( - () => - resolve( [ - { - id: 'meta-1', - content: - 'Explore breathtaking flower and plant photography in our Flora Guide, featuring tips and inspiration for gardening and plant enthusiasts to enhance their outdoor spaces.', - }, - ] ), - 2000 - ) - ); - removeLastMessage(); - } - addMessage( "Here's a suggestion:" ); - setMetaDescriptionOptions( newMetaDescriptions || metaDescriptionOptions ); - }, [ metaDescriptionOptions ] ); - - const handleMetaDescriptionRegenerate = useCallback( async () => { - debug( 'Generating new meta description...' ); - setMetaDescriptionOptions( [] ); - addMessage( ); - const newMetaDescription = await new Promise< Array< Option > >( resolve => - setTimeout( - () => - resolve( [ - { - id: 'meta-1', - content: - 'Explore breathtaking flower and plant photography in our Flora Guide, featuring tips and inspiration for gardening and plant enthusiasts to enhance their outdoor spaces.', - }, - ] ), - 2000 - ) - ); - removeLastMessage(); - addMessage( "Here's a new suggestion:" ); - setMetaDescriptionOptions( newMetaDescription ); - }, [] ); - const handleDone = useCallback( () => { setIsOpen( false ); setCurrentStep( 0 ); setMessages( [] ); }, [] ); + const { + stepProps: keywordsStep, + value: keywords, + setValue: setKeywords, + }: StepHook = useKeywordsStep( { + addMessage, + onStep, + } ); + + const { stepProps: titleStep }: StepHook = useTitleStep( { + addMessage, + removeLastMessage, + onStep, + } ); + + const { stepProps: metaStep }: StepHook = useMetaDescriptionStep( { + addMessage, + removeLastMessage, + onStep, + } ); + const steps: Step[] = [ - { - id: 'keywords', - title: __( 'Optimise for SEO', 'jetpack' ), - messages: [ - { - content: createInterpolateElement( - __( "Hi there! 👋 Let's optimise your blog post for SEO.", 'jetpack' ), - { b: } - ), - showIcon: true, - }, - { - content: createInterpolateElement( - __( - "Here's what we can improve:
1. Keywords
2. Title
3. Meta description", - 'jetpack' - ), - { br:
} - ), - showIcon: false, - }, - { - content: __( - 'To start, please enter 1–3 focus keywords that describe your blog post.', - 'jetpack' - ), - showIcon: true, - }, - ], - type: 'input', - placeholder: __( 'Photography, plants', 'jetpack' ), - onSubmit: handleKeywordsSubmit, - }, - { - id: 'title', - title: __( 'Optimise Title', 'jetpack' ), - messages: [ - { - content: __( "Let's optimise your title.", 'jetpack' ), - showIcon: true, - }, - ], - type: 'options', - options: titleOptions, - onSelect: handleTitleSelect, - onSubmit: handleTitleSubmit, - submitCtaLabel: __( 'Insert', 'jetpack' ), - onRetry: handleTitleRegenerate, - onRetryCtaLabel: __( 'Regenerate', 'jetpack' ), - onStart: handleTitleGenerate, - }, - { - id: 'meta', - title: __( 'Add meta description', 'jetpack' ), - messages: [ - { - content: __( "Now, let's optimize your meta description.", 'jetpack' ), - showIcon: true, - }, - ], - type: 'options', - options: metaDescriptionOptions, - onSelect: handleMetaDescriptionSelect, - onSubmit: handleMetaDescriptionSubmit, - submitCtaLabel: __( 'Insert', 'jetpack' ), - onRetry: handleMetaDescriptionRegenerate, - onRetryCtaLabel: __( 'Regenerate', 'jetpack' ), - onStart: handleMetaDescriptionGenerate, - }, + keywordsStep, + titleStep, + metaStep, { id: 'completion', title: __( 'Your post is SEO-ready', 'jetpack' ), @@ -410,7 +228,10 @@ export default function SeoAssistant( { disabled, onStep }: SeoAssistantProps ) if ( isOpen && messages.length === 0 ) { // Initialize with first step messages currentStepData.messages.forEach( message => - addMessage( message.content, false, message.showIcon ) + addMessage( { + content: message.content, + showIcon: message.showIcon, + } ) ); } }, [ isOpen, currentStepData.messages, messages ] ); @@ -421,7 +242,10 @@ export default function SeoAssistant( { disabled, onStep }: SeoAssistantProps ) setCurrentStep( currentStep + 1 ); // Add next step messages steps[ currentStep + 1 ].messages.forEach( message => - addMessage( message.content, false, message.showIcon ) + addMessage( { + content: message.content, + showIcon: message.showIcon, + } ) ); steps[ currentStep + 1 ].onStart?.(); } @@ -432,7 +256,10 @@ export default function SeoAssistant( { disabled, onStep }: SeoAssistantProps ) setCurrentStep( currentStep - 1 ); // Re-add previous step messages steps[ currentStep - 1 ].messages.forEach( message => - addMessage( message.content, false, message.showIcon ) + addMessage( { + content: message.content, + showIcon: message.showIcon, + } ) ); } }; @@ -484,7 +311,7 @@ export default function SeoAssistant( { disabled, onStep }: SeoAssistantProps ) disabled={ ! selectedOption } > { currentStepData.submitCtaLabel }  - +
); @@ -503,6 +330,27 @@ export default function SeoAssistant( { disabled, onStep }: SeoAssistantProps ) return null; }; + const renderMessageText = message => { + if ( message.type === 'past-options' ) { + return ( +
+ { message.options.map( option => ( +
+ { option.content } +
+ ) ) } +
+ ); + } + + return
{ message.content }
; + }; + const renderMessages = () => { return messages.map( message => (
) }
-
{ message.content }
+ { renderMessageText( message ) }
) ); }; diff --git a/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/seo-assistant/style.scss b/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/seo-assistant/style.scss index c1db241e30f3b..a55d77fb8f03b 100644 --- a/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/seo-assistant/style.scss +++ b/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/seo-assistant/style.scss @@ -106,9 +106,10 @@ &.is-user { background: #f3f4f6; align-self: flex-end; + max-width: 85%; .seo-assistant-wizard__message-icon { - flex-basis: unset; + display: none; } } @@ -167,7 +168,7 @@ &__option { width: 100%; - padding: 16px; + padding: 12px; background: #f3f4f6; border: 2px solid transparent; border-radius: 12px; @@ -177,16 +178,17 @@ line-height: 1.4; transition: all 0.2s ease; animation: messageAppear 0.3s ease-out; - - &:hover { - background: #e5e7eb; - } + color: inherit; &.is-selected { background: #fff; border-color: var( --wp-components-color-accent, var( --wp-admin-theme-color, #007cba ) ); // box-shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1); } + + &:hover:not( div ):not( .is-selected ) { + background: #e5e7eb; + } } &__actions { @@ -231,9 +233,9 @@ // font-size: 14px; // background: #4f46e5; - &:hover { - // background: #4338ca; - } + // &:hover { + // background: #4338ca; + // } } } } diff --git a/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/seo-assistant/use-keywords-step.tsx b/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/seo-assistant/use-keywords-step.tsx new file mode 100644 index 0000000000000..a9e8a9ec510a7 --- /dev/null +++ b/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/seo-assistant/use-keywords-step.tsx @@ -0,0 +1,87 @@ +import { createInterpolateElement, useCallback, useState } from '@wordpress/element'; +import { __, sprintf } from '@wordpress/i18n'; +import type { Step } from './index'; + +export const useKeywordsStep = ( { + addMessage, + onStep, +} ): { + stepProps: Step; + value: string; + setValue: React.Dispatch< React.SetStateAction< string > >; +} => { + const [ keywords, setKeywords ] = useState( '' ); + + const handleSkip = useCallback( () => { + addMessage( __( 'Ok, skipping for now.', 'jetpack' ) ); + }, [ addMessage ] ); + + const handleKeywordsSubmit = useCallback( () => { + addMessage( { content: keywords, isUser: true } ); + if ( ! keywords ) { + return handleSkip(); + } + + const keywordlist = keywords + .split( ',' ) + .map( k => k.trim() ) + .reduce( ( acc, curr, i, arr ) => { + if ( arr.length === 1 ) { + return curr; + } + if ( i === arr.length - 1 ) { + return `${ acc } & ${ curr }`; + } + return i === 0 ? curr : `${ acc }, ${ curr }`; + }, '' ); + const message = createInterpolateElement( + /* Translators: wrapped string is list of keywords user has entered */ + sprintf( __( `Got it! You're targeting %s. ✨✅`, 'jetpack' ), keywordlist ), + { + b: , + } + ); + addMessage( { content: message } ); + if ( onStep ) { + onStep( { value: keywords } ); + } + }, [ onStep, addMessage, keywords, handleSkip ] ); + + return { + stepProps: { + id: 'keywords', + title: __( 'Optimise for SEO', 'jetpack' ), + messages: [ + { + content: createInterpolateElement( + __( "Hi there! 👋 Let's optimise your blog post for SEO.", 'jetpack' ), + { b: } + ), + showIcon: true, + }, + { + content: createInterpolateElement( + __( + "Here's what we can improve:
1. Keywords
2. Title
3. Meta description", + 'jetpack' + ), + { br:
} + ), + showIcon: false, + }, + { + content: __( + 'To start, please enter 1–3 focus keywords that describe your blog post.', + 'jetpack' + ), + showIcon: true, + }, + ], + type: 'input', + placeholder: __( 'Photography, plants', 'jetpack' ), + onSubmit: handleKeywordsSubmit, + }, + value: keywords, + setValue: setKeywords, + }; +}; diff --git a/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/seo-assistant/use-meta-description-step.tsx b/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/seo-assistant/use-meta-description-step.tsx new file mode 100644 index 0000000000000..c64289b72db63 --- /dev/null +++ b/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/seo-assistant/use-meta-description-step.tsx @@ -0,0 +1,105 @@ +import { useCallback, useState } from '@wordpress/element'; +import { __ } from '@wordpress/i18n'; +import { TypingMessage } from './index'; +import type { Step, Option } from './index'; + +export const useMetaDescriptionStep = ( { + addMessage, + removeLastMessage, + onStep, +} ): { + stepProps: Step; + value: string; + setValue: React.Dispatch< React.SetStateAction< string > >; +} => { + // const [ selectedTitle, setSelectedTitle ] = useState< string >(); + // const [ titleOptions, setTitleOptions ] = useState< Option[] >( [] ); + const [ selectedMetaDescription, setSelectedMetaDescription ] = useState< string >(); + const [ metaDescriptionOptions, setMetaDescriptionOptions ] = useState< Option[] >( [] ); + + const handleMetaDescriptionSelect = useCallback( ( option: Option ) => { + setSelectedMetaDescription( option.content ); + setMetaDescriptionOptions( prev => + prev.map( opt => ( { + ...opt, + selected: opt.id === option.id, + } ) ) + ); + }, [] ); + + const handleMetaDescriptionSubmit = useCallback( () => { + addMessage( { content: selectedMetaDescription, isUser: true } ); + addMessage( __( 'Meta description updated! ✅', 'jetpack' ) ); + if ( onStep ) { + onStep( { value: selectedMetaDescription } ); + } + }, [ selectedMetaDescription, onStep, addMessage ] ); + + const handleMetaDescriptionGenerate = useCallback( async () => { + let newMetaDescriptions; + // we only generate if options are empty + if ( metaDescriptionOptions.length === 0 ) { + addMessage( { content: } ); + newMetaDescriptions = await new Promise( resolve => + setTimeout( + () => + resolve( [ + { + id: 'meta-1', + content: + 'Explore breathtaking flower and plant photography in our Flora Guide, featuring tips and inspiration for gardening and plant enthusiasts to enhance their outdoor spaces.', + }, + ] ), + 2000 + ) + ); + removeLastMessage(); + } + addMessage( "Here's a suggestion:" ); + setMetaDescriptionOptions( newMetaDescriptions || metaDescriptionOptions ); + }, [ metaDescriptionOptions, addMessage, removeLastMessage ] ); + + const handleMetaDescriptionRegenerate = useCallback( async () => { + setMetaDescriptionOptions( [] ); + addMessage( { content: } ); + const newMetaDescription = await new Promise< Array< Option > >( resolve => + setTimeout( + () => + resolve( [ + { + id: 'meta-1', + content: + 'Explore breathtaking flower and plant photography in our Flora Guide, featuring tips and inspiration for gardening and plant enthusiasts to enhance their outdoor spaces.', + }, + ] ), + 2000 + ) + ); + removeLastMessage(); + addMessage( "Here's a new suggestion:" ); + setMetaDescriptionOptions( newMetaDescription ); + }, [ addMessage, removeLastMessage ] ); + + return { + stepProps: { + id: 'meta', + title: __( 'Add meta description', 'jetpack' ), + messages: [ + { + content: __( "Now, let's optimize your meta description.", 'jetpack' ), + showIcon: true, + }, + ], + type: 'options', + options: metaDescriptionOptions, + onSelect: handleMetaDescriptionSelect, + onSubmit: handleMetaDescriptionSubmit, + submitCtaLabel: __( 'Insert', 'jetpack' ), + onRetry: handleMetaDescriptionRegenerate, + onRetryCtaLabel: __( 'Regenerate', 'jetpack' ), + onStart: handleMetaDescriptionGenerate, + }, + value: selectedMetaDescription, + setValue: setSelectedMetaDescription, + }; +}; diff --git a/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/seo-assistant/use-title-step.tsx b/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/seo-assistant/use-title-step.tsx new file mode 100644 index 0000000000000..b7c1a0f03852a --- /dev/null +++ b/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/seo-assistant/use-title-step.tsx @@ -0,0 +1,134 @@ +import { useDispatch } from '@wordpress/data'; +import { useCallback, useState } from '@wordpress/element'; +import { __ } from '@wordpress/i18n'; +import { TypingMessage } from './index'; +import type { Step, Option } from './index'; + +export const useTitleStep = ( { + addMessage, + removeLastMessage, + onStep, +} ): { + stepProps: Step; + value: string; + setValue: React.Dispatch< React.SetStateAction< string > >; +} => { + const [ selectedTitle, setSelectedTitle ] = useState< string >(); + const [ titleOptions, setTitleOptions ] = useState< Option[] >( [] ); + const { editPost } = useDispatch( 'core/editor' ); + + const handleTitleSelect = useCallback( ( option: Option ) => { + setSelectedTitle( option.content ); + setTitleOptions( prev => + prev.map( opt => ( { + ...opt, + selected: opt.id === option.id, + } ) ) + ); + }, [] ); + + const handleTitleGenerate = useCallback( async () => { + let newTitles; + // we only generate if options are empty + if ( titleOptions.length === 0 ) { + addMessage( { content: } ); + newTitles = await new Promise( resolve => + setTimeout( + () => + resolve( [ + { + id: '1', + content: 'A Photo Gallery for Gardening Enthusiasths: Flora Guide', + }, + { + id: '2', + content: + 'Flora Guide: Beautiful Photos of Flowers and Plants for Gardening Enthusiasts', + }, + ] ), + 2000 + ) + ); + removeLastMessage(); + } + addMessage( { + content: 'Here are two suggestions based on your keywords. Select the one you prefer:', + } ); + setTitleOptions( newTitles || titleOptions ); + }, [ titleOptions, addMessage, removeLastMessage ] ); + + const replaceOptionsWithFauxUseMessages = useCallback( () => { + const optionsMessage = { + id: 'title-options-' + Math.random(), + content: '', + type: 'past-options', + options: [], + showIcon: false, + }; + // removeLastMessage(); + titleOptions.forEach( titleOption => { + optionsMessage.options.push( { ...titleOption } ); + } ); + addMessage( optionsMessage ); + }, [ titleOptions, addMessage ] ); + + const handleTitleRegenerate = useCallback( async () => { + // This would typically be an async call to generate new titles + replaceOptionsWithFauxUseMessages(); + setTitleOptions( [] ); + addMessage( { content: } ); + const newTitles = await new Promise< Array< Option > >( resolve => + setTimeout( + () => + resolve( [ + { + id: '1', + content: 'A Photo Gallery for Gardening Enthusiasths: Flora Guide', + }, + { + id: '2', + content: + 'Flora Guide: Beautiful Photos of Flowers and Plants for Gardening Enthusiasts', + }, + ] ), + 2000 + ) + ); + removeLastMessage(); + addMessage( 'Here are two new suggestions based on your keywords. Select the one you prefer:' ); + setTitleOptions( newTitles ); + }, [ addMessage, removeLastMessage, replaceOptionsWithFauxUseMessages ] ); + + const handleTitleSubmit = useCallback( () => { + // addMessage( { content: selectedTitle, isUser: true } ); + editPost( { meta: { jetpack_seo_html_title: selectedTitle } } ); + replaceOptionsWithFauxUseMessages(); + addMessage( __( 'Title updated! ✅', 'jetpack' ) ); + if ( onStep ) { + onStep( { value: selectedTitle } ); + } + }, [ selectedTitle, onStep, addMessage, replaceOptionsWithFauxUseMessages, editPost ] ); + + return { + stepProps: { + id: 'title', + title: __( 'Optimise Title', 'jetpack' ), + messages: [ + { + content: __( "Let's optimise your title.", 'jetpack' ), + showIcon: true, + }, + ], + type: 'options', + options: titleOptions, + onSelect: handleTitleSelect, + onSubmit: handleTitleSubmit, + submitCtaLabel: __( 'Insert', 'jetpack' ), + onRetry: handleTitleRegenerate, + onRetryCtaLabel: __( 'Regenerate', 'jetpack' ), + onStart: handleTitleGenerate, + }, + value: selectedTitle, + setValue: setSelectedTitle, + }; +}; From c8214ca4822303e086e36055d868f9ec94b8c11e Mon Sep 17 00:00:00 2001 From: Christian Gastrell Date: Thu, 9 Jan 2025 18:01:10 -0300 Subject: [PATCH 15/29] clean up some commented code --- .../components/seo-assistant/index.tsx | 8 -------- 1 file changed, 8 deletions(-) diff --git a/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/seo-assistant/index.tsx b/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/seo-assistant/index.tsx index 8ba803a2e7081..1fb780b66c6e6 100644 --- a/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/seo-assistant/index.tsx +++ b/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/seo-assistant/index.tsx @@ -48,8 +48,6 @@ interface InputStep extends BaseStep { type: 'input'; placeholder: string; onSubmit: ( value: string ) => void; - // value: string; - // setValue: React.Dispatch< React.SetStateAction< string > >; } interface OptionsStep extends BaseStep { @@ -102,18 +100,12 @@ interface StepMessage { export default function SeoAssistant( { disabled, onStep }: SeoAssistantProps ) { const [ isOpen, setIsOpen ] = useState( false ); const [ currentStep, setCurrentStep ] = useState( 0 ); - // const [ keywords, setKeywords ] = useState( '' ); - // const [ selectedTitle, setSelectedTitle ] = useState< string >(); - // const [ selectedMetaDescription, setSelectedMetaDescription ] = useState< string >(); const [ messages, setMessages ] = useState< Message[] >( [] ); const messagesEndRef = useRef< HTMLDivElement >( null ); - // const [ titleOptions, setTitleOptions ] = useState< Option[] >( [] ); const postContent = usePostContent(); const { isLoadingModules, isChangingStatus, isModuleActive, changeStatus } = useModuleStatus( 'seo-tools' ); - // const [ metaDescriptionOptions, setMetaDescriptionOptions ] = useState< Option[] >( [] ); - const scrollToBottom = () => { messagesEndRef.current?.scrollIntoView( { behavior: 'smooth' } ); }; From 8136de780f341ff45b152830a4b8dfa2e2a0a9b3 Mon Sep 17 00:00:00 2001 From: Christian Gastrell Date: Fri, 10 Jan 2025 19:56:29 -0300 Subject: [PATCH 16/29] temp, completed tracking is failing, need to set complete on done refactor steps schema turn into components, refactor steps add note on completion tracker --- .../components/seo-assistant/index.tsx | 403 +----------------- .../seo-assistant/seo-assistant-wizard.tsx | 328 ++++++++++++++ .../components/seo-assistant/style.scss | 49 +-- .../components/seo-assistant/types.tsx | 71 +++ .../seo-assistant/typing-message.tsx | 11 + .../seo-assistant/use-completion-step.tsx | 70 +++ .../seo-assistant/use-keywords-step.tsx | 88 ++-- .../use-meta-description-step.tsx | 72 ++-- .../seo-assistant/use-title-step.tsx | 103 +++-- .../components/seo-assistant/wizard-input.tsx | 53 +++ 10 files changed, 698 insertions(+), 550 deletions(-) create mode 100644 projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/seo-assistant/seo-assistant-wizard.tsx create mode 100644 projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/seo-assistant/types.tsx create mode 100644 projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/seo-assistant/typing-message.tsx create mode 100644 projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/seo-assistant/use-completion-step.tsx create mode 100644 projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/seo-assistant/wizard-input.tsx diff --git a/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/seo-assistant/index.tsx b/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/seo-assistant/index.tsx index 1fb780b66c6e6..91af40123d3bf 100644 --- a/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/seo-assistant/index.tsx +++ b/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/seo-assistant/index.tsx @@ -1,393 +1,24 @@ import { useModuleStatus } from '@automattic/jetpack-shared-extension-utils'; -import { Button, TextControl, SVG, Circle, Icon } from '@wordpress/components'; -import { - useState, - useCallback, - useEffect, - useRef, - createInterpolateElement, -} from '@wordpress/element'; +import { Button } from '@wordpress/components'; +import { useState } from '@wordpress/element'; import { __ } from '@wordpress/i18n'; -import { arrowRight } from '@wordpress/icons'; -import clsx from 'clsx'; import debugFactory from 'debug'; import { SeoPlaceholder } from '../../../../plugins/seo/components/placeholder'; import usePostContent from '../../hooks/use-post-content'; import './style.scss'; import bigSkyIcon from './big-sky-icon.svg'; -import { useKeywordsStep } from './use-keywords-step'; -import { useMetaDescriptionStep } from './use-meta-description-step'; -import { useTitleStep } from './use-title-step'; - -type StepType = 'input' | 'options' | 'completion'; - -export interface Message { - id?: string; - content?: string | React.ReactNode; - isUser?: boolean; - showIcon?: boolean; - type?: string; - options?: Option[]; -} - -export interface Option { - id: string; - content: string; - selected?: boolean; -} - -interface BaseStep { - id: string; - title: string; - messages: StepMessage[]; - type: StepType; - onStart?: () => void; -} - -interface InputStep extends BaseStep { - type: 'input'; - placeholder: string; - onSubmit: ( value: string ) => void; -} - -interface OptionsStep extends BaseStep { - type: 'options'; - options: Option[]; - onSelect: ( option: Option ) => void; - onSubmit?: () => void; - submitCtaLabel?: string; - onRetry?: () => void; - onRetryCtaLabel?: string; -} - -interface CompletionStep extends BaseStep { - type: 'completion'; -} - -export type Step = InputStep | OptionsStep | CompletionStep; - -interface SeoAssistantProps { - busy?: boolean; - disabled?: boolean; - onStep?: ( data: { value: string | Option | null } ) => void; -} - -type StepHook = { - stepProps: Step; - value: string | Array< string >; - setValue: - | React.Dispatch< React.SetStateAction< string > > - | React.Dispatch< React.SetStateAction< Array< string > > >; -}; +import SeoAssistantWizard from './seo-assistant-wizard'; +import type { SeoAssistantProps } from './types'; const debug = debugFactory( 'jetpack-ai:seo-assistant' ); -export const TypingMessage = () => { - return ( - - - - - - ); -}; - -interface StepMessage { - content: string | React.ReactNode; - showIcon?: boolean; -} - export default function SeoAssistant( { disabled, onStep }: SeoAssistantProps ) { const [ isOpen, setIsOpen ] = useState( false ); - const [ currentStep, setCurrentStep ] = useState( 0 ); - const [ messages, setMessages ] = useState< Message[] >( [] ); - const messagesEndRef = useRef< HTMLDivElement >( null ); const postContent = usePostContent(); const { isLoadingModules, isChangingStatus, isModuleActive, changeStatus } = useModuleStatus( 'seo-tools' ); - const scrollToBottom = () => { - messagesEndRef.current?.scrollIntoView( { behavior: 'smooth' } ); - }; - - useEffect( () => { - scrollToBottom(); - }, [ messages ] ); - - const addMessage = ( message: Message | string ) => { - setMessages( prev => { - const newMessage = { - id: - typeof message === 'string' - ? `message-${ prev.length }` - : message?.id || `message-${ prev.length }`, - content: typeof message === 'string' ? message : message.content, - isUser: typeof message === 'string' ? false : message?.isUser || false, - showIcon: typeof message === 'string' ? true : message?.showIcon ?? ! message.isUser, - type: typeof message === 'string' ? null : message?.type || null, - options: typeof message === 'string' ? [] : message?.options || [], - } as Message; - return [ ...prev, newMessage ]; - } ); - }; - - // const editMessage = useCallback( ( messageId: string, updatedMessage: Partial< Message > ) => { - // setMessages( prev => - // prev.map( message => - // message.id === messageId - // ? { - // ...message, - // ...updatedMessage, - // } - // : message - // ) - // ); - // }, [] ); - - /* Removes last message */ - const removeLastMessage = () => { - setMessages( prev => prev.slice( 0, -1 ) ); - }; - - const handleDone = useCallback( () => { - setIsOpen( false ); - setCurrentStep( 0 ); - setMessages( [] ); - }, [] ); - - const { - stepProps: keywordsStep, - value: keywords, - setValue: setKeywords, - }: StepHook = useKeywordsStep( { - addMessage, - onStep, - } ); - - const { stepProps: titleStep }: StepHook = useTitleStep( { - addMessage, - removeLastMessage, - onStep, - } ); - - const { stepProps: metaStep }: StepHook = useMetaDescriptionStep( { - addMessage, - removeLastMessage, - onStep, - } ); - - const steps: Step[] = [ - keywordsStep, - titleStep, - metaStep, - { - id: 'completion', - title: __( 'Your post is SEO-ready', 'jetpack' ), - messages: [ - { - content: __( "Here's your updated checklist:", 'jetpack' ), - showIcon: true, - }, - { - content: createInterpolateElement( - __( '✅ Keywords
✅ Title
✅ Meta description', 'jetpack' ), - { br:
} - ), - showIcon: false, - }, - { - content: createInterpolateElement( - __( - 'SEO optimization complete! 🎉
Your blog post is now search-engine friendly.', - 'jetpack' - ), - { br:
} - ), - showIcon: true, - }, - { - content: __( 'Happy blogging! 😊', 'jetpack' ), - showIcon: false, - }, - ], - type: 'completion', - }, - ]; - - const currentStepData = steps[ currentStep ]; - - useEffect( () => { - if ( isOpen && messages.length === 0 ) { - // Initialize with first step messages - currentStepData.messages.forEach( message => - addMessage( { - content: message.content, - showIcon: message.showIcon, - } ) - ); - } - }, [ isOpen, currentStepData.messages, messages ] ); - - const handleNext = () => { - if ( currentStep < steps.length - 1 ) { - debug( 'moving to ' + ( currentStep + 1 ), steps[ currentStep + 1 ] ); - setCurrentStep( currentStep + 1 ); - // Add next step messages - steps[ currentStep + 1 ].messages.forEach( message => - addMessage( { - content: message.content, - showIcon: message.showIcon, - } ) - ); - steps[ currentStep + 1 ].onStart?.(); - } - }; - - 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 = () => { - setIsOpen( false ); - setCurrentStep( 0 ); - setMessages( [] ); - }; - - const renderCurrentInput = () => { - if ( currentStepData.type === 'input' ) { - return ( -
- - -
- ); - } - - if ( currentStepData.type === 'options' ) { - const selectedOption = currentStepData.options.find( opt => opt.selected ); - return ( -
- - - -
- ); - } - - if ( currentStepData.type === 'completion' ) { - return ( -
- -
- ); - } - - return null; - }; - - const renderMessageText = message => { - if ( message.type === 'past-options' ) { - return ( -
- { message.options.map( option => ( -
- { option.content } -
- ) ) } -
- ); - } - - return
{ message.content }
; - }; - - const renderMessages = () => { - return messages.map( message => ( -
-
- { message.showIcon && ( - { - ) } -
- { renderMessageText( message ) } -
- ) ); - }; - - const renderOptions = () => { - if ( currentStepData.type !== 'options' || ! currentStepData.options.length ) { - return null; - } - - return ( -
-
-
-
- { currentStepData.options.map( option => ( - - ) ) } -
-
-
- ); - }; - + debug( 'rendering seo-assistant entry point' ); return (

{ __( 'Improve post engagement.', 'jetpack' ) }

@@ -410,29 +41,7 @@ export default function SeoAssistant( { disabled, onStep }: SeoAssistantProps ) changeStatus={ changeStatus } /> ) } - { isOpen && ( -
-
- -

{ currentStepData.title }

- -
- -
-
- { renderMessages() } - { renderOptions() } -
-
- -
{ renderCurrentInput() }
-
-
- ) } + setIsOpen( false ) } />
); } diff --git a/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/seo-assistant/seo-assistant-wizard.tsx b/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/seo-assistant/seo-assistant-wizard.tsx new file mode 100644 index 0000000000000..29108f70e159c --- /dev/null +++ b/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/seo-assistant/seo-assistant-wizard.tsx @@ -0,0 +1,328 @@ +import { + useState, + useCallback, + useEffect, + useRef, + useMemo, + createInterpolateElement, +} from '@wordpress/element'; +import { __ } from '@wordpress/i18n'; +import clsx from 'clsx'; +import debugFactory from 'debug'; +import './style.scss'; +import bigSkyIcon from './big-sky-icon.svg'; +// 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 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 [ monitors, setMonitors ] = useState( [] ); + + const scrollToBottom = () => { + messagesEndRef.current?.scrollIntoView( { behavior: 'smooth' } ); + }; + + useEffect( () => { + scrollToBottom(); + }, [ messages ] ); + + const addMessage = useCallback( async ( message: Message | string ) => { + setMessages( prev => { + const newMessage = { + id: + typeof message === 'string' + ? `message-${ prev.length }` + : message?.id || `message-${ prev.length }`, + content: typeof message === 'string' ? message : message.content, + isUser: typeof message === 'string' ? false : message?.isUser || false, + showIcon: typeof message === 'string' ? true : message?.showIcon ?? ! message.isUser, + type: typeof message === 'string' ? null : message?.type || null, + options: typeof message === 'string' ? [] : message?.options || [], + } as Message; + return [ ...prev, newMessage ]; + } ); + }, [] ); + + /* Removes last message */ + const removeLastMessage = () => { + setMessages( prev => prev.slice( 0, -1 ) ); + }; + + const addMonitor = step => setMonitors( prev => [ ...prev, step ] ); + + const updateMonitor = useCallback( + step => + setMonitors( prev => + prev.map( ( stepMonitor: { id: string } ) => { + return step.id === stepMonitor.id + ? { + ...stepMonitor, + ...step, + } + : stepMonitor; + } ) + ), + [ setMonitors ] + ); + + 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, + { + id: 'completion', + title: __( 'Your post is SEO-ready', 'jetpack' ), + // onStart: handleSummaryChecks, + messages: [ + { + content: __( "Here's your updated checklist:", 'jetpack' ), + showIcon: true, + }, + { + content: createInterpolateElement( + monitors + .map( stepMonitor => + stepMonitor.completed ? `✅ ${ stepMonitor.label }` : `❌ ${ stepMonitor.label }` + ) + .join( '
' ), + { br:
} + ), + showIcon: false, + }, + { + content: createInterpolateElement( + __( + 'SEO optimization complete! 🎉
Your blog post is now search-engine friendly.', + 'jetpack' + ), + { br:
} + ), + showIcon: true, + }, + { + content: __( 'Happy blogging! 😊', 'jetpack' ), + showIcon: false, + }, + ], + type: 'completion', + value: '', + setValue: () => {}, + }, + ], + [ keywordsStep, titleStep, metaStep, monitors ] + ); + + const currentStepData = useMemo( () => steps[ currentStep ], [ steps, currentStep ] ); + + // initialize wizard, set completion monitors + useEffect( () => { + if ( ! isOpen ) { + return; + } + if ( messages.length === 0 ) { + debug( 'init' ); + // Initialize the completion monitor + steps + .filter( step => step.type !== 'completion' ) + .forEach( step => { + addMonitor( { + id: step.id, + label: step.label || step.title, + completed: false, + } ); + } ); + // Initialize with first step messages + currentStepData.messages.forEach( message => + addMessage( { + content: message.content, + showIcon: message.showIcon, + } ) + ); + } + }, [ isOpen, currentStepData.messages, messages, addMessage, steps ] ); + + 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( message => + addMessage( { + content: message.content, + showIcon: message.showIcon, + } ) + ); + steps[ currentStep + 1 ].onStart?.(); + } + }, [ currentStep, steps, setCurrentStep, addMessage ] ); + + const handleSubmit = useCallback( async () => { + await currentStepData.onSubmit?.(); + updateMonitor( { + id: currentStepData.id, + completed: true, + } ); + handleNext(); + }, [ currentStepData, updateMonitor, 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( [] ); + setMonitors( [] ); + }, [ close ] ); + + const renderMessageText = message => { + if ( message.type === 'past-options' ) { + return ( +
+ { message.options.map( option => ( +
+ { option.content } +
+ ) ) } +
+ ); + } + + return
{ message.content }
; + }; + + const renderMessages = () => { + return messages.map( message => ( +
+
+ { message.showIcon && ( + { + ) } +
+ { renderMessageText( message ) } +
+ ) ); + }; + + const renderOptions = () => { + if ( currentStepData.type !== 'options' || ! currentStepData.options.length ) { + return null; + } + + return ( +
+
+
+
+ { currentStepData.options.map( option => ( + + ) ) } +
+
+
+ ); + }; + + return ( + isOpen && ( +
+
+ +

{ currentStepData.title }

+ +
+ +
+
+ { renderMessages() } + { renderOptions() } +
+
+ +
+ +
+
+
+ ) + ); +} diff --git a/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/seo-assistant/style.scss b/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/seo-assistant/style.scss index a55d77fb8f03b..2d95808ad9048 100644 --- a/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/seo-assistant/style.scss +++ b/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/seo-assistant/style.scss @@ -72,9 +72,7 @@ } &__message { - // padding: 12px 16px; border-radius: 16px; - // background: #e0e7ff; white-space: pre-line; animation: messageAppear 0.3s ease-out; font-size: 13px; @@ -84,11 +82,6 @@ min-width: 48px; .seo-assistant-wizard__message-icon { - // height: 26px; - // width: 26px; - // text-align: center; - // line-height: 4; - // flex: 0 1 26px; flex-shrink: 0; align-self: center; flex-basis: 26px; @@ -112,17 +105,12 @@ display: none; } } - - // &:not(.is-user) { - // border-bottom-left-radius: 4px; - // } } &__input-container { flex: 0 0 auto; padding: 16px; background: white; - // border-top: 1px solid #e5e7eb; border-radius: 0 0 16px 16px; border-top: 1px solid var( --jp-gray-5, #e5e7eb ); } @@ -151,7 +139,6 @@ border-radius: 12px; box-shadow: none; outline: 0; - // .components-text-control__input[type=text] } } @@ -172,7 +159,6 @@ background: #f3f4f6; border: 2px solid transparent; border-radius: 12px; - cursor: pointer; text-align: left; font-size: 14px; line-height: 1.4; @@ -183,7 +169,11 @@ &.is-selected { background: #fff; border-color: var( --wp-components-color-accent, var( --wp-admin-theme-color, #007cba ) ); - // box-shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1); + } + + // target buttons only + &:not( div ) { + cursor: pointer; } &:hover:not( div ):not( .is-selected ) { @@ -195,47 +185,19 @@ display: flex; justify-content: flex-end; align-items: center; - // margin-top: 16px; - // padding-top: 16px; - // border-top: 1px solid #e5e7eb; gap: 16px; .components-button { - // height: 40px; - // padding: 8px 16px; border-radius: 20px; - // font-size: 14px; - - // &.is-secondary { - // color: #4b5563; - // } - - // &.is-primary { - // background: #4f46e5; - - // &:hover { - // background: #4338ca; - // } - // } } } &__completion { display: flex; justify-content: flex-end; - // padding-top: 16px; - // border-top: 1px solid #e5e7eb; .components-button { - // height: 40px; - // padding: 8px 24px; border-radius: 20px; - // font-size: 14px; - // background: #4f46e5; - - // &:hover { - // background: #4338ca; - // } } } } @@ -265,6 +227,5 @@ .typing-dot:nth-child(3) { animation-delay: 250ms } .typing-loader { - // background-color: #f1f1f1; color: grey; } diff --git a/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/seo-assistant/types.tsx b/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/seo-assistant/types.tsx new file mode 100644 index 0000000000000..d14a0d2578d19 --- /dev/null +++ b/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/seo-assistant/types.tsx @@ -0,0 +1,71 @@ +type StepType = 'input' | 'options' | 'completion'; + +export interface Message { + id?: string; + content?: string | React.ReactNode; + isUser?: boolean; + showIcon?: boolean; + type?: string; + options?: Option[]; +} + +export interface Option { + id: string; + content: string; + selected?: boolean; +} + +interface BaseStep { + id: string; + title: string; + label?: string; + messages: StepMessage[]; + type: StepType; + onStart?: () => void; + onSubmit?: () => void; + onSkip?: () => void; + value: string; + setValue: + | React.Dispatch< React.SetStateAction< string > > + | React.Dispatch< React.SetStateAction< Array< string > > >; + setCompleted?: React.Dispatch< React.SetStateAction< boolean > >; + completed?: boolean; +} + +interface InputStep extends BaseStep { + type: 'input'; + placeholder: string; +} + +interface OptionsStep extends BaseStep { + type: 'options'; + options: Option[]; + onSelect: ( option: Option ) => void; + submitCtaLabel?: string; + onRetry?: () => void; + onRetryCtaLabel?: string; +} + +interface CompletionStep extends BaseStep { + type: 'completion'; +} + +interface StepMessage { + content: string | React.ReactNode; + showIcon?: boolean; +} + +export type Step = InputStep | OptionsStep | CompletionStep; + +export type CompletionStepHookProps = { + steps: Step[]; + addMessage?: ( message: Message | string ) => void; +}; + +export interface SeoAssistantProps { + isBusy?: boolean; + disabled?: boolean; + onStep?: ( data: { value: string | Option | null } ) => void; + isOpen?: boolean; + close?: () => void; +} diff --git a/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/seo-assistant/typing-message.tsx b/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/seo-assistant/typing-message.tsx new file mode 100644 index 0000000000000..540231b59be73 --- /dev/null +++ b/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/seo-assistant/typing-message.tsx @@ -0,0 +1,11 @@ +import { SVG, Circle } from '@wordpress/components'; + +export default function TypingMessage() { + return ( + + + + + + ); +} diff --git a/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/seo-assistant/use-completion-step.tsx b/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/seo-assistant/use-completion-step.tsx new file mode 100644 index 0000000000000..8e8cee1d8baff --- /dev/null +++ b/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/seo-assistant/use-completion-step.tsx @@ -0,0 +1,70 @@ +import { createInterpolateElement, useCallback } from '@wordpress/element'; +import { __ } from '@wordpress/i18n'; +import type { Step, CompletionStepHookProps } from './types'; + +export const useCompletionStep = ( { addMessage, steps }: CompletionStepHookProps ): Step => { + const getSummaryCheck = useCallback( () => { + const summaryString = steps + .map( step => { + const stepLabel = step.label || step.title; + return step.completed ? `✅ ${ stepLabel }` : `❌ ${ stepLabel }`; + } ) + .join( '
' ); + return createInterpolateElement( summaryString, { br:
} ); + }, [ steps ] ); + + const handleStart = useCallback( async () => { + // await new Promise( resolve => setTimeout( () => resolve( 'done' ), 1000 ) ); + const summary = getSummaryCheck(); + // these were put here because handleNext wouldn't give enough time to update the completed state + addMessage( { content: summary, showIcon: false } ); + addMessage( { + content: createInterpolateElement( + __( + 'SEO optimization complete! 🎉
Your blog post is now search-engine friendly.', + 'jetpack' + ), + { br:
} + ), + showIcon: true, + } ); + addMessage( { + content: __( 'Happy blogging! 😊', 'jetpack' ), + showIcon: false, + } ); + }, [ addMessage, getSummaryCheck ] ); + + return { + id: 'completion', + title: __( 'Your post is SEO-ready', 'jetpack' ), + // onStart: handleSummaryChecks, + messages: [ + { + content: __( "Here's your updated checklist:", 'jetpack' ), + showIcon: true, + }, + { + content: getSummaryCheck(), + showIcon: false, + }, + { + content: createInterpolateElement( + __( + 'SEO optimization complete! 🎉
Your blog post is now search-engine friendly.', + 'jetpack' + ), + { br:
} + ), + showIcon: true, + }, + { + content: __( 'Happy blogging! 😊', 'jetpack' ), + showIcon: false, + }, + ], + type: 'completion', + onStart: handleStart, + value: null, + setValue: () => null, + }; +}; diff --git a/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/seo-assistant/use-keywords-step.tsx b/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/seo-assistant/use-keywords-step.tsx index a9e8a9ec510a7..1222d2ec16114 100644 --- a/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/seo-assistant/use-keywords-step.tsx +++ b/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/seo-assistant/use-keywords-step.tsx @@ -1,26 +1,23 @@ import { createInterpolateElement, useCallback, useState } from '@wordpress/element'; import { __, sprintf } from '@wordpress/i18n'; -import type { Step } from './index'; +import type { Step } from './types'; -export const useKeywordsStep = ( { - addMessage, - onStep, -} ): { - stepProps: Step; - value: string; - setValue: React.Dispatch< React.SetStateAction< string > >; -} => { +export const useKeywordsStep = ( { addMessage, onStep } ): Step => { const [ keywords, setKeywords ] = useState( '' ); + const [ completed, setCompleted ] = useState( false ); const handleSkip = useCallback( () => { - addMessage( __( 'Ok, skipping for now.', 'jetpack' ) ); - }, [ addMessage ] ); + addMessage( __( 'Ok, skipping keywords.', 'jetpack' ) ); + if ( onStep ) { + onStep( { value: '' } ); + } + }, [ addMessage, onStep ] ); const handleKeywordsSubmit = useCallback( () => { - addMessage( { content: keywords, isUser: true } ); - if ( ! keywords ) { + if ( ! keywords.trim() ) { return handleSkip(); } + addMessage( { content: keywords, isUser: true } ); const keywordlist = keywords .split( ',' ) @@ -42,45 +39,48 @@ export const useKeywordsStep = ( { } ); addMessage( { content: message } ); + setCompleted( true ); if ( onStep ) { onStep( { value: keywords } ); } }, [ onStep, addMessage, keywords, handleSkip ] ); return { - stepProps: { - id: 'keywords', - title: __( 'Optimise for SEO', 'jetpack' ), - messages: [ - { - content: createInterpolateElement( - __( "Hi there! 👋 Let's optimise your blog post for SEO.", 'jetpack' ), - { b: } - ), - showIcon: true, - }, - { - content: createInterpolateElement( - __( - "Here's what we can improve:
1. Keywords
2. Title
3. Meta description", - 'jetpack' - ), - { br:
} - ), - showIcon: false, - }, - { - content: __( - 'To start, please enter 1–3 focus keywords that describe your blog post.', + id: 'keywords', + title: __( 'Optimise for SEO', 'jetpack' ), + label: __( 'Keywords', 'jetpack' ), + messages: [ + { + content: createInterpolateElement( + __( "Hi there! 👋 Let's optimise your blog post for SEO.", 'jetpack' ), + { b: } + ), + showIcon: true, + }, + { + content: createInterpolateElement( + __( + "Here's what we can improve:
1. Keywords
2. Title
3. Meta description", 'jetpack' ), - showIcon: true, - }, - ], - type: 'input', - placeholder: __( 'Photography, plants', 'jetpack' ), - onSubmit: handleKeywordsSubmit, - }, + { br:
} + ), + showIcon: false, + }, + { + content: __( + 'To start, please enter 1–3 focus keywords that describe your blog post.', + 'jetpack' + ), + showIcon: true, + }, + ], + type: 'input', + placeholder: __( 'Photography, plants', 'jetpack' ), + onSubmit: handleKeywordsSubmit, + onSkip: handleSkip, + completed, + setCompleted, value: keywords, setValue: setKeywords, }; diff --git a/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/seo-assistant/use-meta-description-step.tsx b/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/seo-assistant/use-meta-description-step.tsx index c64289b72db63..963336cfec372 100644 --- a/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/seo-assistant/use-meta-description-step.tsx +++ b/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/seo-assistant/use-meta-description-step.tsx @@ -1,21 +1,19 @@ +import { useDispatch } from '@wordpress/data'; import { useCallback, useState } from '@wordpress/element'; import { __ } from '@wordpress/i18n'; -import { TypingMessage } from './index'; -import type { Step, Option } from './index'; +import TypingMessage from './typing-message'; +import type { Step, Option } from './types'; export const useMetaDescriptionStep = ( { addMessage, removeLastMessage, onStep, -} ): { - stepProps: Step; - value: string; - setValue: React.Dispatch< React.SetStateAction< string > >; -} => { - // const [ selectedTitle, setSelectedTitle ] = useState< string >(); - // const [ titleOptions, setTitleOptions ] = useState< Option[] >( [] ); + setIsBusy, +} ): Step => { const [ selectedMetaDescription, setSelectedMetaDescription ] = useState< string >(); const [ metaDescriptionOptions, setMetaDescriptionOptions ] = useState< Option[] >( [] ); + const { editPost } = useDispatch( 'core/editor' ); + const [ completed, setCompleted ] = useState( false ); const handleMetaDescriptionSelect = useCallback( ( option: Option ) => { setSelectedMetaDescription( option.content ); @@ -27,15 +25,20 @@ export const useMetaDescriptionStep = ( { ); }, [] ); - const handleMetaDescriptionSubmit = useCallback( () => { + const handleMetaDescriptionSubmit = useCallback( async () => { + addMessage( { content: } ); + await editPost( { meta: { advanced_seo_description: selectedMetaDescription } } ); + removeLastMessage(); addMessage( { content: selectedMetaDescription, isUser: true } ); addMessage( __( 'Meta description updated! ✅', 'jetpack' ) ); + setCompleted( true ); if ( onStep ) { onStep( { value: selectedMetaDescription } ); } - }, [ selectedMetaDescription, onStep, addMessage ] ); + }, [ selectedMetaDescription, onStep, addMessage, editPost, removeLastMessage ] ); const handleMetaDescriptionGenerate = useCallback( async () => { + setIsBusy( true ); let newMetaDescriptions; // we only generate if options are empty if ( metaDescriptionOptions.length === 0 ) { @@ -57,7 +60,8 @@ export const useMetaDescriptionStep = ( { } addMessage( "Here's a suggestion:" ); setMetaDescriptionOptions( newMetaDescriptions || metaDescriptionOptions ); - }, [ metaDescriptionOptions, addMessage, removeLastMessage ] ); + setIsBusy( false ); + }, [ metaDescriptionOptions, addMessage, removeLastMessage, setIsBusy ] ); const handleMetaDescriptionRegenerate = useCallback( async () => { setMetaDescriptionOptions( [] ); @@ -80,26 +84,34 @@ export const useMetaDescriptionStep = ( { setMetaDescriptionOptions( newMetaDescription ); }, [ addMessage, removeLastMessage ] ); + const handleSkip = useCallback( () => { + addMessage( __( 'Ok, leaving your meta description as is and moving on.', 'jetpack' ) ); + if ( onStep ) { + onStep(); + } + }, [ addMessage, onStep ] ); + return { - stepProps: { - id: 'meta', - title: __( 'Add meta description', 'jetpack' ), - messages: [ - { - content: __( "Now, let's optimize your meta description.", 'jetpack' ), - showIcon: true, - }, - ], - type: 'options', - options: metaDescriptionOptions, - onSelect: handleMetaDescriptionSelect, - onSubmit: handleMetaDescriptionSubmit, - submitCtaLabel: __( 'Insert', 'jetpack' ), - onRetry: handleMetaDescriptionRegenerate, - onRetryCtaLabel: __( 'Regenerate', 'jetpack' ), - onStart: handleMetaDescriptionGenerate, - }, + id: 'meta', + title: __( 'Add meta description', 'jetpack' ), + messages: [ + { + content: __( "Now, let's optimize your meta description.", 'jetpack' ), + showIcon: true, + }, + ], + type: 'options', + options: metaDescriptionOptions, + onSelect: handleMetaDescriptionSelect, + onSubmit: handleMetaDescriptionSubmit, + submitCtaLabel: __( 'Insert', 'jetpack' ), + onRetry: handleMetaDescriptionRegenerate, + onRetryCtaLabel: __( 'Regenerate', 'jetpack' ), + onStart: handleMetaDescriptionGenerate, + onSkip: handleSkip, value: selectedMetaDescription, setValue: setSelectedMetaDescription, + completed, + setCompleted, }; }; diff --git a/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/seo-assistant/use-title-step.tsx b/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/seo-assistant/use-title-step.tsx index b7c1a0f03852a..9486ada6ef379 100644 --- a/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/seo-assistant/use-title-step.tsx +++ b/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/seo-assistant/use-title-step.tsx @@ -1,21 +1,20 @@ import { useDispatch } from '@wordpress/data'; -import { useCallback, useState } from '@wordpress/element'; +import { useCallback, useState, useEffect } from '@wordpress/element'; import { __ } from '@wordpress/i18n'; -import { TypingMessage } from './index'; -import type { Step, Option } from './index'; +import TypingMessage from './typing-message'; +import type { Step, Option } from './types'; export const useTitleStep = ( { addMessage, removeLastMessage, onStep, -} ): { - stepProps: Step; - value: string; - setValue: React.Dispatch< React.SetStateAction< string > >; -} => { + contextData, + setIsBusy, +} ): Step => { const [ selectedTitle, setSelectedTitle ] = useState< string >(); const [ titleOptions, setTitleOptions ] = useState< Option[] >( [] ); const { editPost } = useDispatch( 'core/editor' ); + const [ completed, setCompleted ] = useState( false ); const handleTitleSelect = useCallback( ( option: Option ) => { setSelectedTitle( option.content ); @@ -27,7 +26,10 @@ export const useTitleStep = ( { ); }, [] ); + useEffect( () => setTitleOptions( [] ), [ contextData ] ); + const handleTitleGenerate = useCallback( async () => { + setIsBusy( true ); let newTitles; // we only generate if options are empty if ( titleOptions.length === 0 ) { @@ -51,11 +53,18 @@ export const useTitleStep = ( { ); removeLastMessage(); } - addMessage( { - content: 'Here are two suggestions based on your keywords. Select the one you prefer:', - } ); + if ( contextData ) { + addMessage( { + content: 'Here are two suggestions based on your keywords. Select the one you prefer:', + } ); + } else { + addMessage( { + content: 'Here are two suggestions. Select the one you prefer:', + } ); + } setTitleOptions( newTitles || titleOptions ); - }, [ titleOptions, addMessage, removeLastMessage ] ); + setIsBusy( false ); + }, [ titleOptions, addMessage, removeLastMessage, contextData, setIsBusy ] ); const replaceOptionsWithFauxUseMessages = useCallback( () => { const optionsMessage = { @@ -73,6 +82,9 @@ export const useTitleStep = ( { }, [ titleOptions, addMessage ] ); const handleTitleRegenerate = useCallback( async () => { + // let the controller know we're working + setIsBusy( true ); + // This would typically be an async call to generate new titles replaceOptionsWithFauxUseMessages(); setTitleOptions( [] ); @@ -97,38 +109,59 @@ export const useTitleStep = ( { removeLastMessage(); addMessage( 'Here are two new suggestions based on your keywords. Select the one you prefer:' ); setTitleOptions( newTitles ); - }, [ addMessage, removeLastMessage, replaceOptionsWithFauxUseMessages ] ); + setIsBusy( false ); + }, [ addMessage, removeLastMessage, replaceOptionsWithFauxUseMessages, setIsBusy ] ); - const handleTitleSubmit = useCallback( () => { - // addMessage( { content: selectedTitle, isUser: true } ); - editPost( { meta: { jetpack_seo_html_title: selectedTitle } } ); + const handleTitleSubmit = useCallback( async () => { replaceOptionsWithFauxUseMessages(); + addMessage( { content: } ); + await editPost( { title: selectedTitle, meta: { jetpack_seo_html_title: selectedTitle } } ); + removeLastMessage(); addMessage( __( 'Title updated! ✅', 'jetpack' ) ); + setCompleted( true ); if ( onStep ) { onStep( { value: selectedTitle } ); } - }, [ selectedTitle, onStep, addMessage, replaceOptionsWithFauxUseMessages, editPost ] ); + }, [ + selectedTitle, + onStep, + addMessage, + replaceOptionsWithFauxUseMessages, + editPost, + removeLastMessage, + ] ); + + const handleSkip = useCallback( () => { + if ( titleOptions.length ) { + replaceOptionsWithFauxUseMessages(); + } + addMessage( __( 'Ok, leaving the title as is and moving on.', 'jetpack' ) ); + if ( onStep ) { + onStep(); + } + }, [ addMessage, onStep, titleOptions, replaceOptionsWithFauxUseMessages ] ); return { - stepProps: { - id: 'title', - title: __( 'Optimise Title', 'jetpack' ), - messages: [ - { - content: __( "Let's optimise your title.", 'jetpack' ), - showIcon: true, - }, - ], - type: 'options', - options: titleOptions, - onSelect: handleTitleSelect, - onSubmit: handleTitleSubmit, - submitCtaLabel: __( 'Insert', 'jetpack' ), - onRetry: handleTitleRegenerate, - onRetryCtaLabel: __( 'Regenerate', 'jetpack' ), - onStart: handleTitleGenerate, - }, + id: 'title', + title: __( 'Optimise Title', 'jetpack' ), + messages: [ + { + content: __( "Let's optimise your title.", 'jetpack' ), + showIcon: true, + }, + ], + type: 'options', + options: titleOptions, + onSelect: handleTitleSelect, + onSubmit: handleTitleSubmit, + submitCtaLabel: __( 'Insert', 'jetpack' ), + onRetry: handleTitleRegenerate, + onRetryCtaLabel: __( 'Regenerate', 'jetpack' ), + onStart: handleTitleGenerate, + onSkip: handleSkip, value: selectedTitle, setValue: setSelectedTitle, + completed, + setCompleted, }; }; diff --git a/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/seo-assistant/wizard-input.tsx b/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/seo-assistant/wizard-input.tsx new file mode 100644 index 0000000000000..84df47ddbe614 --- /dev/null +++ b/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/seo-assistant/wizard-input.tsx @@ -0,0 +1,53 @@ +import { Button, TextControl, Icon } from '@wordpress/components'; +import { __ } from '@wordpress/i18n'; +import { arrowRight } from '@wordpress/icons'; + +export default function WizardInput( { currentStepData, handleSubmit, handleDone } ) { + if ( currentStepData.type === 'input' ) { + return ( +
+ + +
+ ); + } + + if ( currentStepData.type === 'options' ) { + const selectedOption = currentStepData.options.find( opt => opt.selected ); + return ( +
+ + + +
+ ); + } + + if ( currentStepData.type === 'completion' ) { + return ( +
+ +
+ ); + } + + return null; +} From 57883bfc9397f0297137d997f3f3359d8339c1c7 Mon Sep 17 00:00:00 2001 From: Christian Gastrell Date: Mon, 13 Jan 2025 11:29:59 -0300 Subject: [PATCH 17/29] reinstate completion step hook, nothing works so far --- .../seo-assistant/seo-assistant-wizard.tsx | 142 ++++++++---------- .../seo-assistant/use-completion-step.tsx | 44 +++--- 2 files changed, 81 insertions(+), 105 deletions(-) diff --git a/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/seo-assistant/seo-assistant-wizard.tsx b/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/seo-assistant/seo-assistant-wizard.tsx index 29108f70e159c..9135f7492b393 100644 --- a/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/seo-assistant/seo-assistant-wizard.tsx +++ b/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/seo-assistant/seo-assistant-wizard.tsx @@ -4,14 +4,14 @@ import { useEffect, useRef, useMemo, - createInterpolateElement, + // createInterpolateElement, } from '@wordpress/element'; import { __ } from '@wordpress/i18n'; import clsx from 'clsx'; import debugFactory from 'debug'; import './style.scss'; import bigSkyIcon from './big-sky-icon.svg'; -// import { useCompletionStep } from './use-completion-step'; +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'; @@ -25,7 +25,6 @@ export default function SeoAssistantWizard( { isOpen, close, onStep }: SeoAssist const [ messages, setMessages ] = useState< Message[] >( [] ); const messagesEndRef = useRef< HTMLDivElement >( null ); const [ isBusy, setIsBusy ] = useState( false ); - const [ monitors, setMonitors ] = useState( [] ); const scrollToBottom = () => { messagesEndRef.current?.scrollIntoView( { behavior: 'smooth' } ); @@ -57,23 +56,6 @@ export default function SeoAssistantWizard( { isOpen, close, onStep }: SeoAssist setMessages( prev => prev.slice( 0, -1 ) ); }; - const addMonitor = step => setMonitors( prev => [ ...prev, step ] ); - - const updateMonitor = useCallback( - step => - setMonitors( prev => - prev.map( ( stepMonitor: { id: string } ) => { - return step.id === stepMonitor.id - ? { - ...stepMonitor, - ...step, - } - : stepMonitor; - } ) - ), - [ setMonitors ] - ); - const keywordsStep: Step = useKeywordsStep( { addMessage, onStep, @@ -94,60 +76,36 @@ export default function SeoAssistantWizard( { isOpen, close, onStep }: SeoAssist setIsBusy, } ); - // const completionStep: Step = useCompletionStep( { - // steps: [ keywordsStep, titleStep, metaStep ], - // addMessage, - // } ); + const completionStep: Step = useCompletionStep( { + steps: [ keywordsStep, titleStep, metaStep ], + addMessage, + } ); const steps: Step[] = useMemo( () => [ keywordsStep, titleStep, metaStep, - { - id: 'completion', - title: __( 'Your post is SEO-ready', 'jetpack' ), - // onStart: handleSummaryChecks, - messages: [ - { - content: __( "Here's your updated checklist:", 'jetpack' ), - showIcon: true, - }, - { - content: createInterpolateElement( - monitors - .map( stepMonitor => - stepMonitor.completed ? `✅ ${ stepMonitor.label }` : `❌ ${ stepMonitor.label }` - ) - .join( '
' ), - { br:
} - ), - showIcon: false, - }, - { - content: createInterpolateElement( - __( - 'SEO optimization complete! 🎉
Your blog post is now search-engine friendly.', - 'jetpack' - ), - { br:
} - ), - showIcon: true, - }, - { - content: __( 'Happy blogging! 😊', 'jetpack' ), - showIcon: false, - }, - ], - type: 'completion', - value: '', - setValue: () => {}, - }, + // { + // id: 'completion', + // title: __( 'Your post is SEO-ready', 'jetpack' ), + // // onStart: handleSummaryChecks, + // messages: [ + // { + // content: __( "Here's your updated checklist:", 'jetpack' ), + // showIcon: true, + // }, + // ], + // type: 'completion', + // value: '', + // setValue: () => {}, + // }, + completionStep, ], - [ keywordsStep, titleStep, metaStep, monitors ] + [ keywordsStep, metaStep, titleStep, completionStep ] ); - const currentStepData = useMemo( () => steps[ currentStep ], [ steps, currentStep ] ); + const currentStepData = steps[ currentStep ]; // initialize wizard, set completion monitors useEffect( () => { @@ -156,16 +114,6 @@ export default function SeoAssistantWizard( { isOpen, close, onStep }: SeoAssist } if ( messages.length === 0 ) { debug( 'init' ); - // Initialize the completion monitor - steps - .filter( step => step.type !== 'completion' ) - .forEach( step => { - addMonitor( { - id: step.id, - label: step.label || step.title, - completed: false, - } ); - } ); // Initialize with first step messages currentStepData.messages.forEach( message => addMessage( { @@ -174,7 +122,7 @@ export default function SeoAssistantWizard( { isOpen, close, onStep }: SeoAssist } ) ); } - }, [ isOpen, currentStepData.messages, messages, addMessage, steps ] ); + }, [ isOpen, currentStepData.messages, messages, addMessage ] ); const handleNext = useCallback( () => { if ( currentStep < steps.length - 1 ) { @@ -189,18 +137,44 @@ export default function SeoAssistantWizard( { isOpen, close, onStep }: SeoAssist showIcon: message.showIcon, } ) ); + // If we're on last step, process completion + // if ( currentStep + 1 === steps.length - 1 ) { + // addMessage( { + // content: createInterpolateElement( + // steps + // .filter( step => step.type !== 'completion' ) + // .map( step => { + // const label = step.label || step.title; + // return step.completed ? `✅ ${ label }` : `❌ ${ label }`; + // } ) + // .join( '
' ), + // { br:
} + // ), + // showIcon: false, + // } ); + // addMessage( { + // content: createInterpolateElement( + // __( + // 'SEO optimization complete! 🎉
Your blog post is now search-engine friendly.', + // 'jetpack' + // ), + // { br:
} + // ), + // showIcon: true, + // } ); + // addMessage( { + // content: __( 'Happy blogging! 😊', 'jetpack' ), + // showIcon: false, + // } ); + // } steps[ currentStep + 1 ].onStart?.(); } }, [ currentStep, steps, setCurrentStep, addMessage ] ); const handleSubmit = useCallback( async () => { await currentStepData.onSubmit?.(); - updateMonitor( { - id: currentStepData.id, - completed: true, - } ); handleNext(); - }, [ currentStepData, updateMonitor, handleNext ] ); + }, [ currentStepData, handleNext ] ); const handleBack = () => { if ( currentStep > 0 ) { @@ -225,8 +199,10 @@ export default function SeoAssistantWizard( { isOpen, close, onStep }: SeoAssist close(); setCurrentStep( 0 ); setMessages( [] ); - setMonitors( [] ); - }, [ close ] ); + steps + .filter( step => step.type !== 'completion' ) + .forEach( step => step.setCompleted( false ) ); + }, [ close, steps ] ); const renderMessageText = message => { if ( message.type === 'past-options' ) { diff --git a/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/seo-assistant/use-completion-step.tsx b/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/seo-assistant/use-completion-step.tsx index 8e8cee1d8baff..37b2a63edd008 100644 --- a/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/seo-assistant/use-completion-step.tsx +++ b/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/seo-assistant/use-completion-step.tsx @@ -2,7 +2,7 @@ import { createInterpolateElement, useCallback } from '@wordpress/element'; import { __ } from '@wordpress/i18n'; import type { Step, CompletionStepHookProps } from './types'; -export const useCompletionStep = ( { addMessage, steps }: CompletionStepHookProps ): Step => { +export const useCompletionStep = ( { steps }: CompletionStepHookProps ): Step => { const getSummaryCheck = useCallback( () => { const summaryString = steps .map( step => { @@ -13,26 +13,26 @@ export const useCompletionStep = ( { addMessage, steps }: CompletionStepHookProp return createInterpolateElement( summaryString, { br:
} ); }, [ steps ] ); - const handleStart = useCallback( async () => { - // await new Promise( resolve => setTimeout( () => resolve( 'done' ), 1000 ) ); - const summary = getSummaryCheck(); - // these were put here because handleNext wouldn't give enough time to update the completed state - addMessage( { content: summary, showIcon: false } ); - addMessage( { - content: createInterpolateElement( - __( - 'SEO optimization complete! 🎉
Your blog post is now search-engine friendly.', - 'jetpack' - ), - { br:
} - ), - showIcon: true, - } ); - addMessage( { - content: __( 'Happy blogging! 😊', 'jetpack' ), - showIcon: false, - } ); - }, [ addMessage, getSummaryCheck ] ); + // const handleStart = useCallback( async () => { + // // await new Promise( resolve => setTimeout( () => resolve( 'done' ), 1000 ) ); + // const summary = getSummaryCheck(); + // // these were put here because handleNext wouldn't give enough time to update the completed state + // addMessage( { content: summary, showIcon: false } ); + // addMessage( { + // content: createInterpolateElement( + // __( + // 'SEO optimization complete! 🎉
Your blog post is now search-engine friendly.', + // 'jetpack' + // ), + // { br:
} + // ), + // showIcon: true, + // } ); + // addMessage( { + // content: __( 'Happy blogging! 😊', 'jetpack' ), + // showIcon: false, + // } ); + // }, [ addMessage, getSummaryCheck ] ); return { id: 'completion', @@ -63,7 +63,7 @@ export const useCompletionStep = ( { addMessage, steps }: CompletionStepHookProp }, ], type: 'completion', - onStart: handleStart, + // onStart: handleStart, value: null, setValue: () => null, }; From 5d32e93c87a10ee082f12b4b254fc93b80d6ecc9 Mon Sep 17 00:00:00 2001 From: Christian Gastrell Date: Mon, 13 Jan 2025 13:17:22 -0300 Subject: [PATCH 18/29] better split of the input component --- .../seo-assistant/seo-assistant-wizard.tsx | 12 ++- .../components/seo-assistant/wizard-input.tsx | 86 +++++++++---------- 2 files changed, 47 insertions(+), 51 deletions(-) diff --git a/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/seo-assistant/seo-assistant-wizard.tsx b/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/seo-assistant/seo-assistant-wizard.tsx index 9135f7492b393..a276943b0daa1 100644 --- a/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/seo-assistant/seo-assistant-wizard.tsx +++ b/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/seo-assistant/seo-assistant-wizard.tsx @@ -290,13 +290,11 @@ export default function SeoAssistantWizard( { isOpen, close, onStep }: SeoAssist
-
- -
+
) diff --git a/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/seo-assistant/wizard-input.tsx b/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/seo-assistant/wizard-input.tsx index 84df47ddbe614..aab5e7b47e88d 100644 --- a/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/seo-assistant/wizard-input.tsx +++ b/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/seo-assistant/wizard-input.tsx @@ -3,51 +3,49 @@ import { __ } from '@wordpress/i18n'; import { arrowRight } from '@wordpress/icons'; export default function WizardInput( { currentStepData, handleSubmit, handleDone } ) { - if ( currentStepData.type === 'input' ) { - return ( -
- - -
- ); - } + const selectedOption = + currentStepData.type === 'options' ? currentStepData.options.find( opt => opt.selected ) : null; + return ( +
+ { currentStepData.type === 'input' && ( +
+ + +
+ ) } - if ( currentStepData.type === 'options' ) { - const selectedOption = currentStepData.options.find( opt => opt.selected ); - return ( -
- + { currentStepData.type === 'options' && ( +
+ - -
- ); - } + +
+ ) } - if ( currentStepData.type === 'completion' ) { - return ( -
- -
- ); - } - - return null; + { currentStepData.type === 'completion' && ( +
+ +
+ ) } +
+ ); } From 8fd77c7cbdc33e97d783d9535d60bdbd0001cb7e Mon Sep 17 00:00:00 2001 From: Christian Gastrell Date: Mon, 13 Jan 2025 13:21:28 -0300 Subject: [PATCH 19/29] decouple message rendering to its own component, can be further split though --- .../seo-assistant/seo-assistant-wizard.tsx | 84 +----------------- .../seo-assistant/wizard-messages.tsx | 88 +++++++++++++++++++ 2 files changed, 91 insertions(+), 81 deletions(-) create mode 100644 projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/seo-assistant/wizard-messages.tsx diff --git a/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/seo-assistant/seo-assistant-wizard.tsx b/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/seo-assistant/seo-assistant-wizard.tsx index a276943b0daa1..1996790cda967 100644 --- a/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/seo-assistant/seo-assistant-wizard.tsx +++ b/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/seo-assistant/seo-assistant-wizard.tsx @@ -1,21 +1,13 @@ -import { - useState, - useCallback, - useEffect, - useRef, - useMemo, - // createInterpolateElement, -} from '@wordpress/element'; +import { useState, useCallback, useEffect, useRef, useMemo } from '@wordpress/element'; import { __ } from '@wordpress/i18n'; -import clsx from 'clsx'; import debugFactory from 'debug'; import './style.scss'; -import bigSkyIcon from './big-sky-icon.svg'; 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' ); @@ -204,72 +196,6 @@ export default function SeoAssistantWizard( { isOpen, close, onStep }: SeoAssist .forEach( step => step.setCompleted( false ) ); }, [ close, steps ] ); - const renderMessageText = message => { - if ( message.type === 'past-options' ) { - return ( -
- { message.options.map( option => ( -
- { option.content } -
- ) ) } -
- ); - } - - return
{ message.content }
; - }; - - const renderMessages = () => { - return messages.map( message => ( -
-
- { message.showIcon && ( - { - ) } -
- { renderMessageText( message ) } -
- ) ); - }; - - const renderOptions = () => { - if ( currentStepData.type !== 'options' || ! currentStepData.options.length ) { - return null; - } - - return ( -
-
-
-
- { currentStepData.options.map( option => ( - - ) ) } -
-
-
- ); - }; - return ( isOpen && (
@@ -284,11 +210,7 @@ export default function SeoAssistantWizard( { isOpen, close, onStep }: SeoAssist
-
- { renderMessages() } - { renderOptions() } -
-
+ ( null ); + const scrollToBottom = () => { + messagesEndRef.current?.scrollIntoView( { behavior: 'smooth' } ); + }; + + useEffect( () => { + scrollToBottom(); + }, [ messages ] ); + + const renderMessages = () => + messages.map( message => ( +
+
+ { message.showIcon && ( + { + ) } +
+ { renderMessageText( message ) } +
+ ) ); + + const renderMessageText = message => { + if ( message.type === 'past-options' ) { + return ( +
+ { message.options.map( option => ( +
+ { option.content } +
+ ) ) } +
+ ); + } + + return
{ message.content }
; + }; + + const renderOptions = () => { + if ( currentStepData.type !== 'options' || ! currentStepData.options.length ) { + return null; + } + + return ( +
+
+
+
+ { currentStepData.options.map( option => ( + + ) ) } +
+
+
+ ); + }; + + return ( +
+ { renderMessages() } + { renderOptions() } +
+
+ ); +} From 6ba0a2e71de04de6ca63195cae6da9cb4cb89a10 Mon Sep 17 00:00:00 2001 From: Christian Gastrell Date: Mon, 13 Jan 2025 16:24:30 -0300 Subject: [PATCH 20/29] change skip message copy edit --- .../components/seo-assistant/use-keywords-step.tsx | 2 +- .../components/seo-assistant/use-meta-description-step.tsx | 2 +- .../components/seo-assistant/use-title-step.tsx | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/seo-assistant/use-keywords-step.tsx b/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/seo-assistant/use-keywords-step.tsx index 1222d2ec16114..6c34259a99367 100644 --- a/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/seo-assistant/use-keywords-step.tsx +++ b/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/seo-assistant/use-keywords-step.tsx @@ -7,7 +7,7 @@ export const useKeywordsStep = ( { addMessage, onStep } ): Step => { const [ completed, setCompleted ] = useState( false ); const handleSkip = useCallback( () => { - addMessage( __( 'Ok, skipping keywords.', 'jetpack' ) ); + addMessage( __( 'Skipped!', 'jetpack' ) ); if ( onStep ) { onStep( { value: '' } ); } diff --git a/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/seo-assistant/use-meta-description-step.tsx b/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/seo-assistant/use-meta-description-step.tsx index 963336cfec372..81acc09f70c48 100644 --- a/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/seo-assistant/use-meta-description-step.tsx +++ b/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/seo-assistant/use-meta-description-step.tsx @@ -85,7 +85,7 @@ export const useMetaDescriptionStep = ( { }, [ addMessage, removeLastMessage ] ); const handleSkip = useCallback( () => { - addMessage( __( 'Ok, leaving your meta description as is and moving on.', 'jetpack' ) ); + addMessage( __( 'Skipped!', 'jetpack' ) ); if ( onStep ) { onStep(); } diff --git a/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/seo-assistant/use-title-step.tsx b/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/seo-assistant/use-title-step.tsx index 9486ada6ef379..632f88575d371 100644 --- a/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/seo-assistant/use-title-step.tsx +++ b/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/seo-assistant/use-title-step.tsx @@ -135,7 +135,7 @@ export const useTitleStep = ( { if ( titleOptions.length ) { replaceOptionsWithFauxUseMessages(); } - addMessage( __( 'Ok, leaving the title as is and moving on.', 'jetpack' ) ); + addMessage( __( 'Skipped!', 'jetpack' ) ); if ( onStep ) { onStep(); } From 7993a8ca2201e3310ced55a76218dd1388584abe Mon Sep 17 00:00:00 2001 From: Christian Gastrell Date: Mon, 13 Jan 2025 16:32:12 -0300 Subject: [PATCH 21/29] restore wizard messages scrollbar --- .../components/seo-assistant/style.scss | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/seo-assistant/style.scss b/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/seo-assistant/style.scss index 2d95808ad9048..b761241818ca6 100644 --- a/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/seo-assistant/style.scss +++ b/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/seo-assistant/style.scss @@ -58,16 +58,6 @@ overflow-y: auto; scroll-behavior: smooth; align-items: flex-start; - - /* Hide scrollbar for Chrome, Safari and Opera */ - &::-webkit-scrollbar { - display: none; - } - - /* Hide scrollbar for IE, Edge and Firefox */ - -ms-overflow-style: none; - scrollbar-width: none; - mask-image: linear-gradient( 180deg, transparent, white 24px ); } From bdddd2f96b0beeeb0de3bdb81f3d2622c0406fae Mon Sep 17 00:00:00 2001 From: Christian Gastrell Date: Mon, 13 Jan 2025 17:46:05 -0300 Subject: [PATCH 22/29] add new skip/close button, use wpicons and button components --- .../seo-assistant/seo-assistant-wizard.tsx | 21 +++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/seo-assistant/seo-assistant-wizard.tsx b/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/seo-assistant/seo-assistant-wizard.tsx index 1996790cda967..3c1272336dc10 100644 --- a/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/seo-assistant/seo-assistant-wizard.tsx +++ b/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/seo-assistant/seo-assistant-wizard.tsx @@ -1,5 +1,7 @@ +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'; @@ -200,13 +202,20 @@ export default function SeoAssistantWizard( { isOpen, close, onStep }: SeoAssist isOpen && (
- +

{ currentStepData.title }

- +
+ + + + +
From e10cca0b5efdcf5c58b1cf2b2abd6f721b2c4a16 Mon Sep 17 00:00:00 2001 From: Christian Gastrell Date: Tue, 14 Jan 2025 11:53:51 -0300 Subject: [PATCH 23/29] fix side scrolling issue --- .../ai-assistant-plugin/components/seo-assistant/style.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/seo-assistant/style.scss b/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/seo-assistant/style.scss index b761241818ca6..2d02dc86891a4 100644 --- a/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/seo-assistant/style.scss +++ b/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/seo-assistant/style.scss @@ -83,7 +83,7 @@ .seo-assistant-wizard__message-text { padding: 8px 12px; - flex: 1 0 200px; + // flex: 1 0 200px; } &.is-user { From fb55574f07b1033cb058ad97ed7a602aeb905012 Mon Sep 17 00:00:00 2001 From: Christian Gastrell Date: Tue, 14 Jan 2025 11:54:15 -0300 Subject: [PATCH 24/29] turn messages into components --- .../seo-assistant/wizard-messages.tsx | 111 +++++++++--------- 1 file changed, 55 insertions(+), 56 deletions(-) diff --git a/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/seo-assistant/wizard-messages.tsx b/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/seo-assistant/wizard-messages.tsx index 2d0c31deb6620..fc6e0af1dc98c 100644 --- a/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/seo-assistant/wizard-messages.tsx +++ b/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/seo-assistant/wizard-messages.tsx @@ -3,36 +3,20 @@ import { __ } from '@wordpress/i18n'; import clsx from 'clsx'; import bigSkyIcon from './big-sky-icon.svg'; -export default function Messages( { currentStepData, messages } ) { - const messagesEndRef = useRef< HTMLDivElement >( null ); - const scrollToBottom = () => { - messagesEndRef.current?.scrollIntoView( { behavior: 'smooth' } ); - }; - - useEffect( () => { - scrollToBottom(); - }, [ messages ] ); - - const renderMessages = () => - messages.map( message => ( -
-
- { message.showIcon && ( - { - ) } -
- { renderMessageText( message ) } +const Message = ( { message } ) => { + return ( +
+
+ { message.showIcon && ( + { + ) }
- ) ); - const renderMessageText = message => { - if ( message.type === 'past-options' ) { - return ( + { message.type === 'past-options' && (
{ message.options.map( option => (
) ) }
- ); - } + ) } - return
{ message.content }
; - }; + { ( ! message.type || message.type === 'chat' ) && ( +
{ message.content }
+ ) } +
+ ); +}; - const renderOptions = () => { - if ( currentStepData.type !== 'options' || ! currentStepData.options.length ) { - return null; - } +const OptionMessages = ( { currentStepData } ) => { + if ( currentStepData.type !== 'options' || ! currentStepData.options.length ) { + return null; + } - return ( -
-
-
-
- { currentStepData.options.map( option => ( - - ) ) } -
+ return ( +
+
+
+
+ { currentStepData.options.map( option => ( + + ) ) }
- ); +
+ ); +}; + +export default function Messages( { currentStepData, messages } ) { + const messagesEndRef = useRef< HTMLDivElement >( null ); + const scrollToBottom = () => { + messagesEndRef.current?.scrollIntoView( { behavior: 'smooth' } ); }; + useEffect( () => { + scrollToBottom(); + }, [ messages ] ); + return (
- { renderMessages() } - { renderOptions() } + { messages.map( message => ( + + ) ) } +
); From 29941ce3e3948b21c27a866f814f5c1ded1ac9e7 Mon Sep 17 00:00:00 2001 From: Christian Gastrell Date: Tue, 14 Jan 2025 16:19:45 -0300 Subject: [PATCH 25/29] translate all strings, simplify message object --- .../seo-assistant/seo-assistant-wizard.tsx | 24 +++++++++++-------- .../components/seo-assistant/style.scss | 4 ++-- .../seo-assistant/use-keywords-step.tsx | 2 +- .../use-meta-description-step.tsx | 8 +++---- .../seo-assistant/use-title-step.tsx | 16 +++++++++---- 5 files changed, 33 insertions(+), 21 deletions(-) diff --git a/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/seo-assistant/seo-assistant-wizard.tsx b/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/seo-assistant/seo-assistant-wizard.tsx index 3c1272336dc10..37c80e814c2d5 100644 --- a/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/seo-assistant/seo-assistant-wizard.tsx +++ b/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/seo-assistant/seo-assistant-wizard.tsx @@ -19,6 +19,7 @@ export default function SeoAssistantWizard( { isOpen, close, onStep }: SeoAssist const [ messages, setMessages ] = useState< Message[] >( [] ); const messagesEndRef = useRef< HTMLDivElement >( null ); const [ isBusy, setIsBusy ] = useState( false ); + // const [ messageQueue, setMessageQueue ] = useState( [] ); const scrollToBottom = () => { messagesEndRef.current?.scrollIntoView( { behavior: 'smooth' } ); @@ -28,18 +29,21 @@ export default function SeoAssistantWizard( { isOpen, close, onStep }: SeoAssist scrollToBottom(); }, [ messages ] ); - const addMessage = useCallback( async ( message: Message | string ) => { + // const queueMessage = message => { + // const delay = messageQueue.length * 100 + ( message.delay || 10 ); + // setTimeout( + // () => setMessages( prev => { return [ ...prev, message ]; } ), + // delay + // ); + // setMessageQueue( prev => [ ...prev, message ] ); + // }; + + const addMessage = useCallback( async ( message: Message ) => { setMessages( prev => { const newMessage = { - id: - typeof message === 'string' - ? `message-${ prev.length }` - : message?.id || `message-${ prev.length }`, - content: typeof message === 'string' ? message : message.content, - isUser: typeof message === 'string' ? false : message?.isUser || false, - showIcon: typeof message === 'string' ? true : message?.showIcon ?? ! message.isUser, - type: typeof message === 'string' ? null : message?.type || null, - options: typeof message === 'string' ? [] : message?.options || [], + ...message, + id: message?.id || `message-${ prev.length }`, + showIcon: message.showIcon === false ? false : ! message.isUser, } as Message; return [ ...prev, newMessage ]; } ); diff --git a/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/seo-assistant/style.scss b/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/seo-assistant/style.scss index 2d02dc86891a4..e541444b6a5e1 100644 --- a/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/seo-assistant/style.scss +++ b/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/seo-assistant/style.scss @@ -53,7 +53,7 @@ flex: 1 1 auto; display: flex; flex-direction: column; - gap: 16px; + gap: 8px; padding: 16px 24px; overflow-y: auto; scroll-behavior: smooth; @@ -82,7 +82,7 @@ } .seo-assistant-wizard__message-text { - padding: 8px 12px; + padding: 4px 12px; // flex: 1 0 200px; } diff --git a/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/seo-assistant/use-keywords-step.tsx b/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/seo-assistant/use-keywords-step.tsx index 6c34259a99367..ac811cd2546af 100644 --- a/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/seo-assistant/use-keywords-step.tsx +++ b/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/seo-assistant/use-keywords-step.tsx @@ -7,7 +7,7 @@ export const useKeywordsStep = ( { addMessage, onStep } ): Step => { const [ completed, setCompleted ] = useState( false ); const handleSkip = useCallback( () => { - addMessage( __( 'Skipped!', 'jetpack' ) ); + addMessage( { content: __( 'Skipped!', 'jetpack' ) } ); if ( onStep ) { onStep( { value: '' } ); } diff --git a/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/seo-assistant/use-meta-description-step.tsx b/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/seo-assistant/use-meta-description-step.tsx index 81acc09f70c48..5b4699fd95872 100644 --- a/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/seo-assistant/use-meta-description-step.tsx +++ b/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/seo-assistant/use-meta-description-step.tsx @@ -30,7 +30,7 @@ export const useMetaDescriptionStep = ( { await editPost( { meta: { advanced_seo_description: selectedMetaDescription } } ); removeLastMessage(); addMessage( { content: selectedMetaDescription, isUser: true } ); - addMessage( __( 'Meta description updated! ✅', 'jetpack' ) ); + addMessage( { content: __( 'Meta description updated! ✅', 'jetpack' ) } ); setCompleted( true ); if ( onStep ) { onStep( { value: selectedMetaDescription } ); @@ -58,7 +58,7 @@ export const useMetaDescriptionStep = ( { ); removeLastMessage(); } - addMessage( "Here's a suggestion:" ); + addMessage( { content: __( "Here's a suggestion:", 'jetpack' ) } ); setMetaDescriptionOptions( newMetaDescriptions || metaDescriptionOptions ); setIsBusy( false ); }, [ metaDescriptionOptions, addMessage, removeLastMessage, setIsBusy ] ); @@ -80,12 +80,12 @@ export const useMetaDescriptionStep = ( { ) ); removeLastMessage(); - addMessage( "Here's a new suggestion:" ); + addMessage( { content: __( "Here's a new suggestion:", 'jetpack' ) } ); setMetaDescriptionOptions( newMetaDescription ); }, [ addMessage, removeLastMessage ] ); const handleSkip = useCallback( () => { - addMessage( __( 'Skipped!', 'jetpack' ) ); + addMessage( { content: __( 'Skipped!', 'jetpack' ) } ); if ( onStep ) { onStep(); } diff --git a/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/seo-assistant/use-title-step.tsx b/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/seo-assistant/use-title-step.tsx index 632f88575d371..0153dab8dfaab 100644 --- a/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/seo-assistant/use-title-step.tsx +++ b/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/seo-assistant/use-title-step.tsx @@ -55,11 +55,14 @@ export const useTitleStep = ( { } if ( contextData ) { addMessage( { - content: 'Here are two suggestions based on your keywords. Select the one you prefer:', + content: __( + 'Here are two suggestions based on your keywords. Select the one you prefer:', + 'jetpack' + ), } ); } else { addMessage( { - content: 'Here are two suggestions. Select the one you prefer:', + content: __( 'Here are two suggestions. Select the one you prefer:', 'jetpack' ), } ); } setTitleOptions( newTitles || titleOptions ); @@ -107,7 +110,12 @@ export const useTitleStep = ( { ) ); removeLastMessage(); - addMessage( 'Here are two new suggestions based on your keywords. Select the one you prefer:' ); + addMessage( { + content: __( + 'Here are two new suggestions based on your keywords. Select the one you prefer:', + 'jetpack' + ), + } ); setTitleOptions( newTitles ); setIsBusy( false ); }, [ addMessage, removeLastMessage, replaceOptionsWithFauxUseMessages, setIsBusy ] ); @@ -117,7 +125,7 @@ export const useTitleStep = ( { addMessage( { content: } ); await editPost( { title: selectedTitle, meta: { jetpack_seo_html_title: selectedTitle } } ); removeLastMessage(); - addMessage( __( 'Title updated! ✅', 'jetpack' ) ); + addMessage( { content: __( 'Title updated! ✅', 'jetpack' ) } ); setCompleted( true ); if ( onStep ) { onStep( { value: selectedTitle } ); From 29a675acec2e8f7084e32e215c124cc4ae211ae3 Mon Sep 17 00:00:00 2001 From: Christian Gastrell Date: Wed, 15 Jan 2025 02:22:27 -0300 Subject: [PATCH 26/29] trying to get messages queued --- .../seo-assistant/seo-assistant-wizard.tsx | 57 ++++++++++++------- 1 file changed, 37 insertions(+), 20 deletions(-) diff --git a/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/seo-assistant/seo-assistant-wizard.tsx b/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/seo-assistant/seo-assistant-wizard.tsx index 37c80e814c2d5..a311cec49a591 100644 --- a/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/seo-assistant/seo-assistant-wizard.tsx +++ b/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/seo-assistant/seo-assistant-wizard.tsx @@ -29,24 +29,42 @@ export default function SeoAssistantWizard( { isOpen, close, onStep }: SeoAssist scrollToBottom(); }, [ messages ] ); - // const queueMessage = message => { - // const delay = messageQueue.length * 100 + ( message.delay || 10 ); - // setTimeout( - // () => setMessages( prev => { return [ ...prev, message ]; } ), - // delay - // ); - // setMessageQueue( prev => [ ...prev, message ] ); - // }; + // const queueMessage = useCallback( + // message => { + // setMessageQueue( prev => { + // const delay = prev.length * 200 + ( message.delay || 200 ); + // debug( 'delay', delay ); + // setTimeout( () => { + // debug( 'setting message' ); + // setMessageQueue( prevMessageQueue => [ ...prevMessageQueue.splice( 1 ) ] ); + // setMessages( prevMessages => { + // return [ ...prevMessages, { ...message, id: `message-${ prevMessages.length }` } ]; + // } ); + // }, delay ); + // return [ ...prev, { ...message, id: `message-${ prev.length }` } ]; + // } ); + // }, + // [ setMessages, setMessageQueue ] + // ); const addMessage = useCallback( async ( message: Message ) => { + const newMessage = { + ...message, + showIcon: message.showIcon === false ? false : ! message.isUser, + } as Message; + setMessages( prev => { - const newMessage = { - ...message, - id: message?.id || `message-${ prev.length }`, - showIcon: message.showIcon === false ? false : ! message.isUser, - } as Message; - return [ ...prev, newMessage ]; + return [ ...prev, { ...newMessage, id: `message-${ prev.length }` } ]; } ); + + // if ( newMessage.isUser ) { + // setMessages( prev => { + // return [ ...prev, { ...newMessage, id: `message-${ prev.length }` } ]; + // } ); + // } else { + // debug( 'queueing message' ); + // queueMessage( newMessage ); + // } }, [] ); /* Removes last message */ @@ -110,15 +128,14 @@ export default function SeoAssistantWizard( { isOpen, close, onStep }: SeoAssist 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( message => - addMessage( { - content: message.content, - showIcon: message.showIcon, - } ) - ); + currentStepData.messages.forEach( message => { + debug( 'adding initial message' ); + addMessage( message ); + } ); } }, [ isOpen, currentStepData.messages, messages, addMessage ] ); From e34721dd32f46bc23ae0401c4ca2faf0b9f2483d Mon Sep 17 00:00:00 2001 From: Christian Gastrell Date: Wed, 15 Jan 2025 14:14:56 -0300 Subject: [PATCH 27/29] turn props to ints, failing linte --- .../components/seo-assistant/seo-assistant-wizard.tsx | 6 +++--- .../components/seo-assistant/wizard-input.tsx | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/seo-assistant/seo-assistant-wizard.tsx b/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/seo-assistant/seo-assistant-wizard.tsx index a311cec49a591..905407ac43bc9 100644 --- a/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/seo-assistant/seo-assistant-wizard.tsx +++ b/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/seo-assistant/seo-assistant-wizard.tsx @@ -224,17 +224,17 @@ export default function SeoAssistantWizard( { isOpen, close, onStep }: SeoAssist

{ currentStepData.title }

diff --git a/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/seo-assistant/wizard-input.tsx b/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/seo-assistant/wizard-input.tsx index aab5e7b47e88d..f9263e0188088 100644 --- a/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/seo-assistant/wizard-input.tsx +++ b/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/seo-assistant/wizard-input.tsx @@ -34,7 +34,7 @@ export default function WizardInput( { currentStepData, handleSubmit, handleDone
) } From 27307c5282f8ec9ee7d44c329f15649373d39c10 Mon Sep 17 00:00:00 2001 From: Christian Gastrell Date: Wed, 15 Jan 2025 14:15:34 -0300 Subject: [PATCH 28/29] testing solutions for step tracker --- .../components/seo-assistant/seo-assistant-wizard.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/seo-assistant/seo-assistant-wizard.tsx b/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/seo-assistant/seo-assistant-wizard.tsx index 905407ac43bc9..5037138501c01 100644 --- a/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/seo-assistant/seo-assistant-wizard.tsx +++ b/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/seo-assistant/seo-assistant-wizard.tsx @@ -1,4 +1,5 @@ import { Button, Icon, Tooltip } from '@wordpress/components'; +// import { useDebounce } from '@wordpress/compose'; import { useState, useCallback, useEffect, useRef, useMemo } from '@wordpress/element'; import { __ } from '@wordpress/i18n'; import { next, closeSmall, chevronLeft } from '@wordpress/icons'; @@ -121,7 +122,7 @@ export default function SeoAssistantWizard( { isOpen, close, onStep }: SeoAssist [ keywordsStep, metaStep, titleStep, completionStep ] ); - const currentStepData = steps[ currentStep ]; + const currentStepData = useMemo( () => steps[ currentStep ], [ steps, currentStep ] ); // initialize wizard, set completion monitors useEffect( () => { From f69072ef430b1bc140c59fc4f5c6dffdd17052a0 Mon Sep 17 00:00:00 2001 From: Christian Gastrell Date: Thu, 16 Jan 2025 15:21:02 -0300 Subject: [PATCH 29/29] cleanup commented code, use isEditedPostEmpty as a way to disable the button --- .../components/seo-assistant/index.tsx | 7 +- .../seo-assistant/seo-assistant-wizard.tsx | 96 +------------------ .../seo-assistant/use-completion-step.tsx | 21 ---- 3 files changed, 8 insertions(+), 116 deletions(-) diff --git a/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/seo-assistant/index.tsx b/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/seo-assistant/index.tsx index 91af40123d3bf..167dfa8d948c0 100644 --- a/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/seo-assistant/index.tsx +++ b/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/seo-assistant/index.tsx @@ -1,10 +1,11 @@ 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 usePostContent from '../../hooks/use-post-content'; import './style.scss'; import bigSkyIcon from './big-sky-icon.svg'; import SeoAssistantWizard from './seo-assistant-wizard'; @@ -14,7 +15,7 @@ const debug = debugFactory( 'jetpack-ai:seo-assistant' ); export default function SeoAssistant( { disabled, onStep }: SeoAssistantProps ) { const [ isOpen, setIsOpen ] = useState( false ); - const postContent = usePostContent(); + const postIsEmpty = useSelect( select => select( editorStore ).isEditedPostEmpty(), [] ); const { isLoadingModules, isChangingStatus, isModuleActive, changeStatus } = useModuleStatus( 'seo-tools' ); @@ -26,7 +27,7 @@ export default function SeoAssistant( { disabled, onStep }: SeoAssistantProps )