{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 && (
-
-
- {__('Insert', 'mind')} ⏎
+ {insertionPlace === 'selected-blocks' &&
+ hasNonEmptySelectedBlocks() && (
+ onInsert('insert')}>
+ {__('Insert', 'mind')} ⏎
+
+ )}
+
+ {insertionPlace === 'selected-blocks' &&
+ hasNonEmptySelectedBlocks()
+ ? __('Replace Selected Blocks', 'mind')
+ : __('Insert', '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) {
);
}
diff --git a/src/editor/popup/components/input/style.scss b/src/editor/popup/components/input/style.scss
index 17071d6..eead4ee 100644
--- a/src/editor/popup/components/input/style.scss
+++ b/src/editor/popup/components/input/style.scss
@@ -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;
- }
}
diff --git a/src/editor/popup/components/loading-text/style.scss b/src/editor/popup/components/loading-text/style.scss
index d5e6c68..602c1fb 100644
--- a/src/editor/popup/components/loading-text/style.scss
+++ b/src/editor/popup/components/loading-text/style.scss
@@ -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 {
diff --git a/src/editor/popup/index.js b/src/editor/popup/index.js
index b551fe8..55776f7 100644
--- a/src/editor/popup/index.js
+++ b/src/editor/popup/index.js
@@ -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);
diff --git a/src/editor/store/popup/actions.js b/src/editor/store/popup/actions.js
index 2c7afef..9fa2a6d 100644
--- a/src/editor/store/popup/actions.js
+++ b/src/editor/store/popup/actions.js
@@ -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
diff --git a/src/editor/store/popup/reducer.js b/src/editor/store/popup/reducer.js
index 6399106..371f0ff 100644
--- a/src/editor/store/popup/reducer.js
+++ b/src/editor/store/popup/reducer.js
@@ -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':
diff --git a/src/editor/store/popup/selectors.js b/src/editor/store/popup/selectors.js
index fa31206..a8ceb9f 100644
--- a/src/editor/store/popup/selectors.js
+++ b/src/editor/store/popup/selectors.js
@@ -7,7 +7,7 @@ export function getInput(state) {
}
export function getContext(state) {
- return state?.context || '';
+ return state?.context || [];
}
export function getInsertionPlace(state) {
diff --git a/src/utils/clean-block-json/index.js b/src/utils/clean-block-json/index.js
new file mode 100644
index 0000000..b450d39
--- /dev/null
+++ b/src/utils/clean-block-json/index.js
@@ -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;
+}
diff --git a/src/utils/get-page-blocks-json/index.js b/src/utils/get-page-blocks-json/index.js
new file mode 100644
index 0000000..e9b7b28
--- /dev/null
+++ b/src/utils/get-page-blocks-json/index.js
@@ -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;
+}
diff --git a/src/utils/get-page-context-json/index.js b/src/utils/get-page-context-json/index.js
new file mode 100644
index 0000000..d9704bc
--- /dev/null
+++ b/src/utils/get-page-context-json/index.js
@@ -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;
+}
diff --git a/src/utils/get-selected-blocks-json/index.js b/src/utils/get-selected-blocks-json/index.js
index abb2a26..a28fcf8 100644
--- a/src/utils/get-selected-blocks-json/index.js
+++ b/src/utils/get-selected-blocks-json/index.js
@@ -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;
}
diff --git a/src/utils/has-non-empty-selected-blocks/index.js b/src/utils/has-non-empty-selected-blocks/index.js
new file mode 100644
index 0000000..4cca85c
--- /dev/null
+++ b/src/utils/has-non-empty-selected-blocks/index.js
@@ -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;
+}