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
This commit is contained in:
Nikita 2024-12-28 15:26:25 +03:00
parent f054220647
commit e4df5e290e
20 changed files with 459 additions and 239 deletions

View file

@ -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>' . $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>' . $context . '</context>',
];
if ( $selected_blocks ) {
$user_query .= "\n";
$user_query .= '<selected_blocks_context>' . $selected_blocks . '</selected_blocks_context>';
}
if ( $page_blocks ) {
$user_query .= "\n";
$user_query .= '<page_blocks_context>' . $page_blocks . '</page_blocks_context>';
}
if ( $page_context ) {
$user_query .= "\n";
$user_query .= '<page_context>' . $page_context . '</page_context>';
}
// User Query.
$messages[] = [
'role' => 'user',
'content' => '<user_query>' . $user_query . '</user_query>',
return [
[
'role' => 'system',
'content' => Mind_Prompts::get_system_prompt(),
],
[
'role' => 'user',
'content' => $user_query,
],
];
return $messages;
}
/**

View file

@ -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
</format_rules>
<core_rules>
<operation_rules>
- 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
</core_rules>
</operation_rules>
<context_rules>
- When context is provided:
- IMPORTANT: Return ALL context blocks
<contexts>
<selected_blocks_context>
- 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
</context_rules>
</selected_blocks_context>
<page_blocks_context>
- 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
</page_blocks_context>
<page_context>
- Additional page information for reference only
</page_context>
<site_context>
- Global site information and guidelines
- Apply to all generated content
- Match tone and terminology
- Follow brand requirements
- Use provided business information
</site_context>
</contexts>
<block_supports_features>
These features are shared across many blocks and include:

View file

@ -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 );
}
/**

View file

@ -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) {

View file

@ -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');
}
}}

View file

@ -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: <PopupPostTitleAboutIcon />,
},
{
type: 'request',
label: __('Post about…', 'mind'),
request: __('Write a blog post about ', 'mind'),
icon: <PopupPostAboutIcon />,
},
{
type: 'request',
label: __('Outline about…', 'mind'),
request: __('Write a blog post outline about ', 'mind'),
icon: <PopupOutlineAboutIcon />,
},
{
type: 'category',
label: __('Content Presets', 'mind'),
},
{
type: 'request',
label: __('Paragraph about…', 'mind'),
request: __('Create a paragraph about ', 'mind'),
icon: <PopupParagraphAboutIcon />,
},
{
type: 'request',
label: __('List about…', 'mind'),
request: __('Create a list about ', 'mind'),
icon: <PopupListAboutIcon />,
},
{
type: 'request',
label: __('Table about…', 'mind'),
request: __('Create a table about ', 'mind'),
icon: <PopupTableAboutIcon />,
},
];
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 (
<div className="mind-popup-content">
{screen === '' ? (
<div className="mind-popup-commands">
{commands.map((data) => {
if (data.type === 'category') {
return (
<span
key={data.type + data.label}
className="mind-popup-commands-category"
>
{data.label}
</span>
);
}
return (
<Button
key={data.type + data.label}
className="mind-popup-commands-button"
onClick={() => {
setInput(data.request);
setScreen('request');
}}
>
{data.icon || ''}
{data.label}
</Button>
);
})}
</div>
) : null}
{screen === 'request' && (
<div className="mind-popup-request">
{response?.length > 0 && (

View file

@ -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 (
<div className="mind-popup-footer">
{loading && <LoadingText>{__('Writing', 'mind')}</LoadingText>}
<div>
<div className="mind-popup-footer-context">
{availableContexts.map((item) => {
if (!item) {
return null;
}
if (
!editableContexts &&
!context.includes(item.value)
) {
return null;
}
return (
<Tooltip
delay={500}
placement="top"
key={item.value}
text={item.tooltip}
>
<button
key={item.value}
disabled={!editableContexts}
className={
context.includes(item.value)
? 'active'
: ''
}
onClick={() => {
if (context.includes(item.value)) {
setContext(
context.filter(
(ctx) => ctx !== item.value
)
);
} else {
setContext([
...context,
item.value,
]);
}
}}
>
{item.name}
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
width="14"
height="14"
fill="currentColor"
>
<path d="M19 11h-6V5h-2v6H5v2h6v6h2v-6h6z" />
</svg>
</button>
</Tooltip>
);
})}
</div>
</div>
<div className="mind-popup-footer-actions">
{input && !loading && response?.length === 0 && (
<Button
onClick={() => {
requestAI();
}}
{!loading && response?.length === 0 && (
<Tooltip
placement="top"
text={__('Ask AI and get answer', 'mind')}
>
{__('Get Answer', 'mind')} <kbd></kbd>
<Button
className="mind-popup-footer-actions-primary mind-popup-footer-actions-icon"
onClick={() => {
requestAI();
}}
aria-label={__('Get Answer', 'mind')}
disabled={!input}
>
</Button>
</Tooltip>
)}
{loading && (
<Button
className="mind-popup-footer-actions-primary"
disabled
>
<LoadingText>{__('Loading', 'mind')}</LoadingText>
</Button>
)}
{response?.length > 0 && !loading && (
@ -72,8 +163,21 @@ export default function Input(props) {
>
{__('Copy', 'mind')}
</Button>
<Button onClick={onInsert}>
{__('Insert', 'mind')} <kbd></kbd>
{insertionPlace === 'selected-blocks' &&
hasNonEmptySelectedBlocks() && (
<Button onClick={() => onInsert('insert')}>
{__('Insert', 'mind')} <kbd></kbd>
</Button>
)}
<Button
className="mind-popup-footer-actions-primary"
onClick={onInsert}
>
{insertionPlace === 'selected-blocks' &&
hasNonEmptySelectedBlocks()
? __('Replace Selected Blocks', 'mind')
: __('Insert', 'mind')}{' '}
<kbd></kbd>
</Button>
</>
)}

View file

@ -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;
}
}
}

View file

@ -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) {
<MindLogoIcon />
<textarea
ref={ref}
placeholder={__('Ask AI to write anything…', 'mind')}
placeholder={__('Ask AI to build or change blocks…', 'mind')}
value={input}
onChange={(e) => {
setInput(e.target.value);
@ -114,9 +103,6 @@ export default function Input(props) {
disabled={loading}
rows={1}
/>
{contextLabel ? (
<span className="mind-popup-input-context">{contextLabel}</span>
) : null}
</div>
);
}

View file

@ -2,9 +2,9 @@ $padding: 10px;
.mind-popup-input {
display: flex;
flex-wrap: wrap;
align-items: center;
margin-bottom: 0;
border-bottom: 1px solid #e8e7e7;
> svg {
position: absolute;
@ -33,14 +33,4 @@ $padding: 10px;
color: #a3a3a3;
}
}
.mind-popup-input-context {
font-weight: 400;
border-radius: 3px;
padding: 3px 10px;
margin-right: 10px;
color: rgb(0, 0, 0, 60%);
background-color: rgba(0, 0, 0, 8%);
white-space: nowrap;
}
}

View file

@ -1,6 +1,9 @@
.mind-popup-loading-text::after {
content: "...";
animation: mind-popup-loading-text 2s infinite;
display: inline-block;
text-align: left;
min-width: 13px;
animation: mind-popup-loading-text 1s infinite;
}
@keyframes mind-popup-loading-text {

View file

@ -20,6 +20,7 @@ import LoadingLine from './components/loading-line';
import Content from './components/content';
import Footer from './components/footer';
import NotConnectedScreen from './components/not-connected-screen';
import hasNonEmptySelectedBlocks from '../../utils/has-non-empty-selected-blocks';
const POPUP_CONTAINER_CLASS = 'mind-popup-container';
@ -60,9 +61,15 @@ export default function Popup() {
const { insertBlocks: wpInsertBlocks, replaceBlocks } =
useDispatch('core/block-editor');
function insertBlocks() {
function insertBlocks(customPlace) {
if (response.length) {
if (insertionPlace === 'selected-blocks') {
if (customPlace && customPlace === 'insert') {
wpInsertBlocks(response);
} else if (
insertionPlace === 'selected-blocks' &&
selectedClientIds &&
selectedClientIds.length
) {
replaceBlocks(selectedClientIds, response);
} else {
wpInsertBlocks(response);

View file

@ -8,7 +8,10 @@ import apiFetch from '@wordpress/api-fetch';
* Internal dependencies.
*/
import BlocksStreamProcessor from '../../processors/blocks-stream-processor';
import getPageBlocksJSON from '../../../utils/get-page-blocks-json';
import getPageContextJSON from '../../../utils/get-page-context-json';
import getSelectedBlocksJSON from '../../../utils/get-selected-blocks-json';
import hasNonEmptySelectedBlocks from '../../../utils/has-non-empty-selected-blocks';
import { isConnected } from '../core/selectors';
export function open() {
@ -103,9 +106,18 @@ export function requestAI() {
request: select.getInput(),
};
// Add context if needed
if (select.getContext() === 'selected-blocks') {
data.context = getSelectedBlocksJSON();
// Add selected_blocks context if needed
if (
select.getContext().includes('selected-blocks') &&
hasNonEmptySelectedBlocks()
) {
data.selected_blocks = getSelectedBlocksJSON(true);
}
// Add page context if needed
if (select.getContext().includes('page')) {
data.page_blocks = getPageBlocksJSON(true);
data.page_context = getPageContextJSON(true);
}
// Initialize stream processor

View file

@ -1,7 +1,7 @@
const initialState = {
isOpen: false,
input: '',
context: '',
context: ['selected-blocks', 'page'],
insertionPlace: '',
screen: '',
loading: false,
@ -26,10 +26,19 @@ function reducer(state = initialState, action = {}) {
break;
case 'OPEN':
if (!state.isOpen) {
return {
const newState = {
...state,
isOpen: true,
};
// Always set context to selected blocks when open popup.
// In case the blocks are not selected or a single empty paragraph,
// we will not send the context to the AI.
if (!newState.context.includes('selected-blocks')) {
newState.context = [...newState.context, 'selected-blocks'];
}
return newState;
}
break;
case 'TOGGLE':

View file

@ -7,7 +7,7 @@ export function getInput(state) {
}
export function getContext(state) {
return state?.context || '';
return state?.context || [];
}
export function getInsertionPlace(state) {

View file

@ -0,0 +1,24 @@
export default function cleanBlockJSON(block) {
if (block?.attributes?.metadata) {
delete block.attributes.metadata;
}
if (block?.attributes?.patternName) {
delete block.attributes.patternName;
}
const cleanedBlock = {
clientId: block.clientId,
name: block.name,
attributes: block.attributes,
innerBlocks: [],
};
if (block.innerBlocks.length) {
block.innerBlocks.forEach((innerBlock) => {
cleanedBlock.innerBlocks.push(cleanBlockJSON(innerBlock));
});
}
return cleanedBlock;
}

View file

@ -0,0 +1,20 @@
import cleanBlockJSON from '../clean-block-json';
export default function getPageBlocksJSON(stringify) {
const { getBlocks } = wp.data.select('core/block-editor');
const allBlocks = getBlocks();
const blocksJSON = [];
allBlocks.forEach((blockData) => {
if (blockData?.name && blockData?.attributes) {
blocksJSON.push(cleanBlockJSON(blockData));
}
});
if (stringify) {
return JSON.stringify(blocksJSON);
}
return blocksJSON;
}

View file

@ -0,0 +1,22 @@
export default function getPageContextJSON(stringify) {
const { getCurrentPost } = wp.data.select('core/editor');
const currentPost = getCurrentPost();
const result = {
title: currentPost.title,
type: currentPost.type,
slug: currentPost.slug,
link: currentPost.link,
status: currentPost.status,
date: currentPost.date_gmt,
modified: currentPost.modified_gmt,
excerpt: currentPost.excerpt,
};
if (stringify) {
return JSON.stringify(result);
}
return result;
}

View file

@ -1,4 +1,6 @@
export default function getSelectedBlocksJSON() {
import cleanBlockJSON from '../clean-block-json';
export default function getSelectedBlocksJSON(stringify) {
const { getBlock, getSelectedBlockClientIds } =
wp.data.select('core/block-editor');
@ -9,14 +11,13 @@ export default function getSelectedBlocksJSON() {
const blockData = getBlock(id);
if (blockData?.name && blockData?.attributes) {
blocksJSON.push({
clientId: blockData.clientId,
name: blockData.name,
attributes: blockData.attributes,
innerBlocks: blockData?.innerBlocks || [],
});
blocksJSON.push(cleanBlockJSON(blockData));
}
});
return JSON.stringify(blocksJSON);
if (stringify) {
return JSON.stringify(blocksJSON);
}
return blocksJSON;
}

View file

@ -0,0 +1,20 @@
import getSelectedBlocksJSON from '../get-selected-blocks-json';
export default function hasNonEmptySelectedBlocks() {
const selectedBlocks = getSelectedBlocksJSON();
if (!selectedBlocks || !selectedBlocks.length) {
return false;
}
// In case selected an empty paragraph, return false.
if (
selectedBlocks.length === 1 &&
selectedBlocks[0].name === 'core/paragraph' &&
!selectedBlocks[0].attributes.content.trim()
) {
return false;
}
return true;
}