From e4df5e290e358bd304f38fa1de42b80bf7363583 Mon Sep 17 00:00:00 2001 From: Nikita Date: Sat, 28 Dec 2024 15:26:25 +0300 Subject: [PATCH] changed popup and work with contexts add possibility to provide current page context and selected blocks context simplified popup by removing suggestions on initial load simplified popup footer styles and --- classes/class-ai-api.php | 53 +++--- classes/class-prompts.php | 42 +++-- classes/class-rest.php | 8 +- src/editor/extensions/block-toolbar/index.js | 3 +- src/editor/extensions/post-toolbar/index.js | 9 +- src/editor/popup/components/content/index.js | 91 +--------- src/editor/popup/components/footer/index.js | 164 ++++++++++++++---- src/editor/popup/components/footer/style.scss | 106 ++++++++++- src/editor/popup/components/input/index.js | 58 +++---- src/editor/popup/components/input/style.scss | 12 +- .../popup/components/loading-text/style.scss | 5 +- src/editor/popup/index.js | 11 +- src/editor/store/popup/actions.js | 18 +- src/editor/store/popup/reducer.js | 13 +- src/editor/store/popup/selectors.js | 2 +- src/utils/clean-block-json/index.js | 24 +++ src/utils/get-page-blocks-json/index.js | 20 +++ src/utils/get-page-context-json/index.js | 22 +++ src/utils/get-selected-blocks-json/index.js | 17 +- .../has-non-empty-selected-blocks/index.js | 20 +++ 20 files changed, 459 insertions(+), 239 deletions(-) create mode 100644 src/utils/clean-block-json/index.js create mode 100644 src/utils/get-page-blocks-json/index.js create mode 100644 src/utils/get-page-context-json/index.js create mode 100644 src/utils/has-non-empty-selected-blocks/index.js diff --git a/classes/class-ai-api.php b/classes/class-ai-api.php index 2165649..6d00618 100644 --- a/classes/class-ai-api.php +++ b/classes/class-ai-api.php @@ -101,11 +101,13 @@ class Mind_AI_API { * Send request to API. * * @param string $request request text. - * @param string $context context. + * @param string $selected_blocks selected blocks context. + * @param string $page_blocks page blocks context. + * @param string $page_context page context. * * @return mixed */ - public function request( $request, $context ) { + public function request( $request, $selected_blocks = '', $page_blocks = '', $page_context = '' ) { // Set headers for streaming. header( 'Content-Type: text/event-stream' ); header( 'Cache-Control: no-cache' ); @@ -127,7 +129,7 @@ class Mind_AI_API { exit; } - $messages = $this->prepare_messages( $request, $context ); + $messages = $this->prepare_messages( $request, $selected_blocks, $page_blocks, $page_context ); if ( 'gpt-4o' === $connected_model['name'] || 'gpt-4o-mini' === $connected_model['name'] ) { $this->request_open_ai( $connected_model, $messages ); @@ -142,31 +144,36 @@ class Mind_AI_API { * Prepare messages for request. * * @param string $user_query user query. - * @param string $context context. + * @param string $selected_blocks selected blocks context. + * @param string $page_blocks page blocks context. + * @param string $page_context page context. */ - public function prepare_messages( $user_query, $context ) { - $messages = []; + public function prepare_messages( $user_query, $selected_blocks, $page_blocks, $page_context ) { + $user_query = '' . $user_query . ''; - $messages[] = [ - 'role' => 'system', - 'content' => Mind_Prompts::get_system_prompt( $user_query, $context ), - ]; - - // Optional blocks JSON context. - if ( $context ) { - $messages[] = [ - 'role' => 'user', - 'content' => '' . $context . '', - ]; + if ( $selected_blocks ) { + $user_query .= "\n"; + $user_query .= '' . $selected_blocks . ''; + } + if ( $page_blocks ) { + $user_query .= "\n"; + $user_query .= '' . $page_blocks . ''; + } + if ( $page_context ) { + $user_query .= "\n"; + $user_query .= '' . $page_context . ''; } - // User Query. - $messages[] = [ - 'role' => 'user', - 'content' => '' . $user_query . '', + return [ + [ + 'role' => 'system', + 'content' => Mind_Prompts::get_system_prompt(), + ], + [ + 'role' => 'user', + 'content' => $user_query, + ], ]; - - return $messages; } /** diff --git a/classes/class-prompts.php b/classes/class-prompts.php index 18ee4f1..68189d3 100644 --- a/classes/class-prompts.php +++ b/classes/class-prompts.php @@ -16,11 +16,9 @@ class Mind_Prompts { /** * Get system prompt. * - * @param WP_REST_Request $request Request object. - * @param string $context Context. * @return string */ - public static function get_system_prompt( $request, $context ) { + public static function get_system_prompt() { return ' You are Mind - an elite WordPress architect specializing in building high-converting websites with WordPress page builder, optimized UX patterns, and enterprise-level development practices. @@ -38,7 +36,7 @@ You are Mind - an elite WordPress architect specializing in building high-conver - Maintain proper hierarchy - + - Content focus: - Address user request primarily - Enhance related elements when needed @@ -58,17 +56,37 @@ You are Mind - an elite WordPress architect specializing in building high-conver - Asking questions - Using placeholder content - Breaking functionality - + - - - When context is provided: - - IMPORTANT: Return ALL context blocks + + + - These blocks are selected for direct modification + - IMPORTANT: Return ALL these blocks in response - Preserve structure and attributes + - Modify based on user query - Maintain links and media - - Enhance requested elements - - Adjust related content as needed - - Never remove context blocks - + + + + - Current page blocks for reference only + - DO NOT modify these blocks + - Use as style and structure reference + - Match patterns when creating new content + - Ensure visual consistency + + + + - Additional page information for reference only + + + + - Global site information and guidelines + - Apply to all generated content + - Match tone and terminology + - Follow brand requirements + - Use provided business information + + These features are shared across many blocks and include: diff --git a/classes/class-rest.php b/classes/class-rest.php index 2737e64..09c6b7c 100644 --- a/classes/class-rest.php +++ b/classes/class-rest.php @@ -115,10 +115,12 @@ class Mind_Rest extends WP_REST_Controller { * @return mixed */ public function request_ai( WP_REST_Request $req ) { - $request = $req->get_param( 'request' ) ?? ''; - $context = $req->get_param( 'context' ) ?? ''; + $request = $req->get_param( 'request' ) ?? ''; + $selected_blocks = $req->get_param( 'selected_blocks' ) ?? ''; + $page_blocks = $req->get_param( 'page_blocks' ) ?? ''; + $page_context = $req->get_param( 'page_context' ) ?? ''; - Mind_AI_API::instance()->request( $request, $context ); + Mind_AI_API::instance()->request( $request, $selected_blocks, $page_blocks, $page_context ); } /** diff --git a/src/editor/extensions/block-toolbar/index.js b/src/editor/extensions/block-toolbar/index.js index 2c381fc..588cb85 100644 --- a/src/editor/extensions/block-toolbar/index.js +++ b/src/editor/extensions/block-toolbar/index.js @@ -63,13 +63,12 @@ const LANGUAGE = [ ]; function Toolbar() { - const { open, setInput, setContext, setInsertionPlace, requestAI } = + const { open, setInput, setInsertionPlace, requestAI } = useDispatch('mind/popup'); function openModal(prompt) { open(); setInput(prompt); - setContext('selected-blocks'); setInsertionPlace('selected-blocks'); if (prompt) { diff --git a/src/editor/extensions/post-toolbar/index.js b/src/editor/extensions/post-toolbar/index.js index 1ab657a..0432e63 100644 --- a/src/editor/extensions/post-toolbar/index.js +++ b/src/editor/extensions/post-toolbar/index.js @@ -18,7 +18,7 @@ import { ReactComponent as MindLogoIcon } from '../../../icons/mind-logo.svg'; const TOOLBAR_TOGGLE_CONTAINER_CLASS = 'mind-post-toolbar-toggle'; function Toggle() { - const { open, setContext, setInsertionPlace } = useDispatch('mind/popup'); + const { open, setInsertionPlace } = useDispatch('mind/popup'); const { getSelectedBlockClientIds } = useSelect((select) => { return { @@ -37,12 +37,7 @@ function Toggle() { open(); const selectedIDs = getSelectedBlockClientIds(); - - // This is a temporary solution to provide context when multiple blocks selected. - // We need this because Gutenberg does not provide an ability to add toolbar button when multiple selection. - // We actually should make a toggle in `extensions/block-toolbar/index.js` to handle this case. - if (selectedIDs && selectedIDs.length > 1) { - setContext('selected-blocks'); + if (selectedIDs && selectedIDs.length) { setInsertionPlace('selected-blocks'); } }} diff --git a/src/editor/popup/components/content/index.js b/src/editor/popup/components/content/index.js index 5bdc789..4c1a2df 100644 --- a/src/editor/popup/components/content/index.js +++ b/src/editor/popup/components/content/index.js @@ -3,82 +3,25 @@ import './style.scss'; /** * WordPress dependencies */ -import { __ } from '@wordpress/i18n'; import { useRef, useEffect } from '@wordpress/element'; import { useSelect, useDispatch } from '@wordpress/data'; -import { Button } from '@wordpress/components'; /** * Internal dependencies */ import Notice from '../notice'; import AIResponse from '../ai-response'; -import { ReactComponent as PopupPostTitleAboutIcon } from '../../../../icons/popup-post-title-about.svg'; -import { ReactComponent as PopupPostAboutIcon } from '../../../../icons/popup-post-about.svg'; -import { ReactComponent as PopupOutlineAboutIcon } from '../../../../icons/popup-outline-about.svg'; -import { ReactComponent as PopupParagraphAboutIcon } from '../../../../icons/popup-paragraph-about.svg'; -import { ReactComponent as PopupListAboutIcon } from '../../../../icons/popup-list-about.svg'; -import { ReactComponent as PopupTableAboutIcon } from '../../../../icons/popup-table-about.svg'; - -const commands = [ - { - type: 'category', - label: __('Post Presets', 'mind'), - }, - { - type: 'request', - label: __('Post title about…', 'mind'), - request: __('Write a post title about ', 'mind'), - icon: , - }, - { - type: 'request', - label: __('Post about…', 'mind'), - request: __('Write a blog post about ', 'mind'), - icon: , - }, - { - type: 'request', - label: __('Outline about…', 'mind'), - request: __('Write a blog post outline about ', 'mind'), - icon: , - }, - - { - type: 'category', - label: __('Content Presets', 'mind'), - }, - { - type: 'request', - label: __('Paragraph about…', 'mind'), - request: __('Create a paragraph about ', 'mind'), - icon: , - }, - { - type: 'request', - label: __('List about…', 'mind'), - request: __('Create a list about ', 'mind'), - icon: , - }, - { - type: 'request', - label: __('Table about…', 'mind'), - request: __('Create a table about ', 'mind'), - icon: , - }, -]; export default function Content() { const ref = useRef(); - const { setInput, setScreen } = useDispatch('mind/popup'); + const { setScreen } = useDispatch('mind/popup'); const { isOpen, input, screen, loading, response, progress, error } = useSelect((select) => { const { isOpen: checkIsOpen, getInput, - getContext, getScreen, getLoading, getResponse, @@ -89,7 +32,6 @@ export default function Content() { return { isOpen: checkIsOpen(), input: getInput(), - context: getContext(), screen: getScreen(), loading: getLoading(), response: getResponse(), @@ -124,37 +66,6 @@ export default function Content() { return (
- {screen === '' ? ( -
- {commands.map((data) => { - if (data.type === 'category') { - return ( - - {data.label} - - ); - } - - return ( - - ); - })} -
- ) : null} - {screen === 'request' && (
{response?.length > 0 && ( diff --git a/src/editor/popup/components/footer/index.js b/src/editor/popup/components/footer/index.js index 8e403bb..40f7eed 100644 --- a/src/editor/popup/components/footer/index.js +++ b/src/editor/popup/components/footer/index.js @@ -4,49 +4,140 @@ import './style.scss'; * WordPress dependencies */ import { __ } from '@wordpress/i18n'; -import { Button } from '@wordpress/components'; +import { Button, Tooltip } from '@wordpress/components'; import { useSelect, useDispatch } from '@wordpress/data'; +import hasNonEmptySelectedBlocks from '../../../../utils/has-non-empty-selected-blocks'; import LoadingText from '../loading-text'; export default function Input(props) { const { onInsert } = props; - const { close, reset, setError, requestAI } = useDispatch('mind/popup'); + const { close, reset, setContext, setError, requestAI } = + useDispatch('mind/popup'); - const { input, loading, response } = useSelect((select) => { - const { getInput, getContext, getScreen, getLoading, getResponse } = - select('mind/popup'); + const { input, context, loading, response, insertionPlace } = useSelect( + (select) => { + const { + getInput, + getContext, + getLoading, + getResponse, + getInsertionPlace, + } = select('mind/popup'); - return { - input: getInput(), - context: getContext(), - screen: getScreen(), - loading: getLoading(), - response: getResponse(), - }; - }); + return { + input: getInput(), + context: getContext(), + loading: getLoading(), + response: getResponse(), + insertionPlace: getInsertionPlace(), + }; + } + ); - const showFooter = - response?.length > 0 || - loading || - (input && !loading && response?.length === 0); - - if (!showFooter) { - return null; - } + const availableContexts = [ + { + name: __('Page', 'mind'), + tooltip: __('Provide page context', 'mind'), + value: 'page', + }, + hasNonEmptySelectedBlocks() + ? { + name: __('Selected Blocks', 'mind'), + tooltip: __('Provide selected blocks context', 'mind'), + value: 'selected-blocks', + } + : false, + ]; + const editableContexts = !loading && !response?.length; return (
- {loading && {__('Writing', 'mind')}} +
+
+ {availableContexts.map((item) => { + if (!item) { + return null; + } + + if ( + !editableContexts && + !context.includes(item.value) + ) { + return null; + } + + return ( + + + + ); + })} +
+
- {input && !loading && response?.length === 0 && ( - + + )} + {loading && ( + )} {response?.length > 0 && !loading && ( @@ -72,8 +163,21 @@ export default function Input(props) { > {__('Copy', 'mind')} - + )} + )} diff --git a/src/editor/popup/components/footer/style.scss b/src/editor/popup/components/footer/style.scss index 2bd1925..6bb46b6 100644 --- a/src/editor/popup/components/footer/style.scss +++ b/src/editor/popup/components/footer/style.scss @@ -1,23 +1,85 @@ -$padding: 10px; - .mind-popup-footer { display: flex; justify-content: space-between; align-items: center; - padding: $padding $padding * 2; - background-color: #f9f9f9; - border-top: 1px solid #e8e7e7; + padding: 20px; + padding-top: 0; + + .mind-popup-content:has(iframe) + & { + padding-top: 20px; + } +} +.mind-popup-footer-context { + display: flex; + gap: 8px; + width: 100%; + + > * { + position: relative; + display: flex; + align-items: center; + gap: 15px; + font-weight: 500; + border-radius: 5px; + padding: 5px 10px; + padding-right: 5px; + white-space: nowrap; + background: none; + cursor: pointer; + color: rgb(0, 0, 0, 40%); + border: 1px dashed rgba(0, 0, 0, 15%); + + &::after { + content: ""; + position: absolute; + top: 0; + right: 23px; + bottom: 0; + border-right: 1px dashed rgba(0, 0, 0, 15%); + } + + &:hover, + &:focus-visible { + color: rgb(0, 0, 0, 50%); + background: rgba(0, 0, 0, 3%); + border-color: rgba(0, 0, 0, 20%); + } + + &.active { + color: #121212; + border-style: solid; + + &::after { + border-right-style: solid; + } + + svg { + transform: rotate(45deg); + } + } + + &:disabled { + padding-right: 10px; + color: rgba(0, 0, 0, 20%); + border-color: rgba(0, 0, 0, 10%); + pointer-events: none; + + &::after, + svg { + display: none; + } + } + } } .mind-popup-footer-actions { display: flex; - margin: -5px -15px; margin-left: auto; gap: 5px; button { display: flex; gap: 8px; - height: 28px; + height: 27px; border-radius: 5px; font-weight: 500; @@ -32,9 +94,37 @@ $padding: 10px; font-weight: 400; border-radius: 3px; padding: 3px 4px; - margin-right: -8px; + margin-right: -9px; color: rgba(0, 0, 0, 50%); background-color: rgba(0, 0, 0, 8%); } + + &.mind-popup-footer-actions-primary { + background-color: #000; + color: #fff; + + &:hover, + &:focus-visible { + background-color: #212121; + color: #fff; + } + + kbd { + color: #fff; + background-color: #535353; + } + + &:disabled { + color: rgba(0, 0, 0, 20%); + border: 1px solid rgba(0, 0, 0, 7%); + pointer-events: none; + background-color: rgba(0, 0, 0, 2%); + } + } + &.mind-popup-footer-actions-icon { + width: 27px; + padding: 0; + justify-content: center; + } } } diff --git a/src/editor/popup/components/input/index.js b/src/editor/popup/components/input/index.js index d2e38e4..65fefc5 100644 --- a/src/editor/popup/components/input/index.js +++ b/src/editor/popup/components/input/index.js @@ -19,40 +19,25 @@ export default function Input(props) { const { reset, setInput, setScreen, requestAI } = useDispatch('mind/popup'); - const { isOpen, input, context, screen, loading, response } = useSelect( - (select) => { - const { - isOpen: checkIsOpen, - getInput, - getContext, - getScreen, - getLoading, - getResponse, - } = select('mind/popup'); + const { isOpen, input, screen, loading, response } = useSelect((select) => { + const { + isOpen: checkIsOpen, + getInput, + getScreen, + getLoading, + getResponse, + } = select('mind/popup'); - return { - isOpen: checkIsOpen(), - input: getInput(), - context: getContext(), - screen: getScreen(), - loading: getLoading(), - response: getResponse(), - }; - } - ); + return { + isOpen: checkIsOpen(), + input: getInput(), + screen: getScreen(), + loading: getLoading(), + response: getResponse(), + }; + }); const hasResponse = response?.length > 0; - let contextLabel = context; - - switch (context) { - case 'selected-blocks': - contextLabel = __('Selected Blocks'); - break; - case 'post-title': - contextLabel = __('Post Title'); - break; - // no default - } function onKeyDown(e) { // Go back to starter screen. @@ -69,7 +54,11 @@ export default function Input(props) { // Send request to AI. if (screen === 'request' && e.key === 'Enter' && !e.shiftKey) { - requestAI(); + e.preventDefault(); + + if (input) { + requestAI(); + } } } @@ -105,7 +94,7 @@ export default function Input(props) {