2023-06-02 22:31:31 +03:00
|
|
|
/**
|
|
|
|
* Styles
|
|
|
|
*/
|
|
|
|
import './style.scss';
|
|
|
|
|
|
|
|
/**
|
|
|
|
* WordPress dependencies
|
|
|
|
*/
|
|
|
|
import { __ } from '@wordpress/i18n';
|
2023-08-02 15:21:14 +03:00
|
|
|
import { createRoot, useRef, useEffect, RawHTML } from '@wordpress/element';
|
|
|
|
import { Modal, Button, TextControl } from '@wordpress/components';
|
2023-06-02 22:31:31 +03:00
|
|
|
import { useSelect, useDispatch } from '@wordpress/data';
|
2023-08-02 15:21:14 +03:00
|
|
|
import { rawHandler } from '@wordpress/blocks';
|
2023-06-02 22:31:31 +03:00
|
|
|
import domReady from '@wordpress/dom-ready';
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Internal dependencies
|
|
|
|
*/
|
|
|
|
import TOOLBAR_ICON from '../utils/icon';
|
2023-08-02 15:21:14 +03:00
|
|
|
import LoadingLine from './components/loading-line';
|
|
|
|
import LoadingText from './components/loading-text';
|
|
|
|
import Notice from './components/notice';
|
2023-06-02 22:31:31 +03:00
|
|
|
|
2023-08-02 15:21:14 +03:00
|
|
|
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';
|
2023-06-02 22:31:31 +03:00
|
|
|
|
2023-08-02 15:21:14 +03:00
|
|
|
const POPUP_CONTAINER_CLASS = 'mind-popup-container';
|
2023-06-02 22:31:31 +03:00
|
|
|
|
2023-08-02 15:21:14 +03:00
|
|
|
const commands = [
|
2023-06-02 22:31:31 +03:00
|
|
|
{
|
|
|
|
type: 'category',
|
2023-08-02 15:21:14 +03:00
|
|
|
label: __('Post Presets', 'mind'),
|
2023-06-02 22:31:31 +03:00
|
|
|
},
|
|
|
|
{
|
2023-08-02 15:21:14 +03:00
|
|
|
type: 'request',
|
|
|
|
label: __('Post title about…', 'mind'),
|
|
|
|
request: __('Write a post title about ', 'mind'),
|
|
|
|
icon: <PopupPostTitleAboutIcon />,
|
2023-06-02 22:31:31 +03:00
|
|
|
},
|
|
|
|
{
|
2023-08-02 15:21:14 +03:00
|
|
|
type: 'request',
|
|
|
|
label: __('Post about…', 'mind'),
|
|
|
|
request: __('Write a blog post about ', 'mind'),
|
|
|
|
icon: <PopupPostAboutIcon />,
|
2023-06-02 22:31:31 +03:00
|
|
|
},
|
|
|
|
{
|
2023-08-02 15:21:14 +03:00
|
|
|
type: 'request',
|
|
|
|
label: __('Outline about…', 'mind'),
|
|
|
|
request: __('Write a blog post outline about ', 'mind'),
|
|
|
|
icon: <PopupOutlineAboutIcon />,
|
2023-06-02 22:31:31 +03:00
|
|
|
},
|
|
|
|
|
|
|
|
{
|
|
|
|
type: 'category',
|
2023-08-02 15:21:14 +03:00
|
|
|
label: __('Content Presets', 'mind'),
|
2023-06-02 22:31:31 +03:00
|
|
|
},
|
|
|
|
{
|
2023-08-02 15:21:14 +03:00
|
|
|
type: 'request',
|
|
|
|
label: __('Paragraph about…', 'mind'),
|
|
|
|
request: __('Create a paragraph about ', 'mind'),
|
|
|
|
icon: <PopupParagraphAboutIcon />,
|
2023-06-02 22:31:31 +03:00
|
|
|
},
|
|
|
|
{
|
2023-08-02 15:21:14 +03:00
|
|
|
type: 'request',
|
|
|
|
label: __('List about…', 'mind'),
|
|
|
|
request: __('Create a list about ', 'mind'),
|
|
|
|
icon: <PopupListAboutIcon />,
|
2023-06-02 22:31:31 +03:00
|
|
|
},
|
|
|
|
{
|
2023-08-02 15:21:14 +03:00
|
|
|
type: 'request',
|
|
|
|
label: __('Table about…', 'mind'),
|
|
|
|
request: __('Create a table about ', 'mind'),
|
|
|
|
icon: <PopupTableAboutIcon />,
|
2023-06-02 22:31:31 +03:00
|
|
|
},
|
|
|
|
];
|
|
|
|
|
|
|
|
export default function Popup(props) {
|
|
|
|
const { onClose } = props;
|
|
|
|
|
2023-08-02 15:21:14 +03:00
|
|
|
const ref = useRef();
|
|
|
|
|
|
|
|
const { setHighlightBlocks } = useDispatch('mind/blocks');
|
2023-06-02 22:31:31 +03:00
|
|
|
|
2023-08-02 15:21:14 +03:00
|
|
|
const { close, reset, setInput, setScreen, setError, requestAI } =
|
|
|
|
useDispatch('mind/popup');
|
2023-06-02 22:31:31 +03:00
|
|
|
|
2023-08-02 15:21:14 +03:00
|
|
|
const {
|
|
|
|
isOpen,
|
|
|
|
input,
|
|
|
|
context,
|
|
|
|
replaceBlocks,
|
|
|
|
screen,
|
|
|
|
loading,
|
|
|
|
response,
|
|
|
|
error,
|
|
|
|
} = useSelect((select) => {
|
|
|
|
const {
|
|
|
|
isOpen: checkIsOpen,
|
|
|
|
getInput,
|
|
|
|
getContext,
|
|
|
|
getReplaceBlocks,
|
|
|
|
getScreen,
|
|
|
|
getLoading,
|
|
|
|
getResponse,
|
|
|
|
getError,
|
|
|
|
} = select('mind/popup');
|
|
|
|
|
|
|
|
return {
|
|
|
|
isOpen: checkIsOpen(),
|
|
|
|
input: getInput(),
|
|
|
|
context: getContext(),
|
|
|
|
replaceBlocks: getReplaceBlocks(),
|
|
|
|
screen: getScreen(),
|
|
|
|
loading: getLoading(),
|
|
|
|
response: getResponse(),
|
|
|
|
error: getError(),
|
|
|
|
};
|
2023-06-02 22:31:31 +03:00
|
|
|
});
|
|
|
|
|
2023-08-02 15:21:14 +03:00
|
|
|
let contextLabel = context;
|
|
|
|
|
|
|
|
switch (context) {
|
|
|
|
case 'selected-blocks':
|
|
|
|
contextLabel = __('Selected Blocks');
|
|
|
|
break;
|
|
|
|
case 'post-title':
|
|
|
|
contextLabel = __('Post Title');
|
|
|
|
break;
|
|
|
|
// no default
|
|
|
|
}
|
|
|
|
|
|
|
|
const { insertBlocks: wpInsertBlocks, replaceBlocks: wpReplaceBlocks } =
|
|
|
|
useDispatch('core/block-editor');
|
|
|
|
|
|
|
|
function focusInput() {
|
|
|
|
if (ref?.current) {
|
|
|
|
const inputEl = ref.current.querySelector(
|
|
|
|
'.mind-popup-input input'
|
|
|
|
);
|
|
|
|
|
|
|
|
if (inputEl) {
|
|
|
|
inputEl.focus();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
function copyToClipboard() {
|
|
|
|
window.navigator.clipboard.writeText(response);
|
|
|
|
}
|
|
|
|
|
|
|
|
function insertResponse() {
|
|
|
|
const parsedBlocks = rawHandler({ HTML: response });
|
|
|
|
|
|
|
|
if (parsedBlocks.length) {
|
|
|
|
if (replaceBlocks && replaceBlocks.length) {
|
|
|
|
wpReplaceBlocks(replaceBlocks, parsedBlocks);
|
|
|
|
} else {
|
|
|
|
wpInsertBlocks(parsedBlocks);
|
|
|
|
}
|
|
|
|
|
|
|
|
setHighlightBlocks(
|
|
|
|
parsedBlocks.map((data) => {
|
|
|
|
return data.clientId;
|
|
|
|
})
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-08-02 16:48:06 +03:00
|
|
|
function onInsert() {
|
|
|
|
insertResponse();
|
|
|
|
|
|
|
|
reset();
|
|
|
|
close();
|
|
|
|
|
|
|
|
if (onClose) {
|
|
|
|
onClose();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-08-02 15:21:14 +03:00
|
|
|
// Set focus on Input.
|
|
|
|
useEffect(() => {
|
2023-08-02 16:48:06 +03:00
|
|
|
if (isOpen && !loading && ref?.current) {
|
2023-08-02 15:21:14 +03:00
|
|
|
focusInput();
|
|
|
|
}
|
2023-08-02 16:48:06 +03:00
|
|
|
}, [isOpen, loading, ref]);
|
2023-08-02 15:21:14 +03:00
|
|
|
|
|
|
|
// Open request page if something is in input.
|
|
|
|
useEffect(() => {
|
|
|
|
if (screen === '' && input) {
|
|
|
|
setScreen('request');
|
|
|
|
}
|
|
|
|
}, [screen, input, setScreen]);
|
|
|
|
|
2023-08-02 16:48:06 +03:00
|
|
|
function onKeyDown(e) {
|
|
|
|
// Go back to starter screen.
|
|
|
|
if (screen !== '' && e.key === 'Backspace' && !e.target.value) {
|
|
|
|
reset();
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Insert request to post.
|
|
|
|
if (response && e.key === 'Enter') {
|
|
|
|
onInsert();
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Send request to AI.
|
|
|
|
if (screen === 'request' && e.key === 'Enter') {
|
|
|
|
requestAI();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-06-02 22:31:31 +03:00
|
|
|
if (!isOpen) {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
2023-08-02 15:21:14 +03:00
|
|
|
const showFooter = response || (input && !loading && !response);
|
|
|
|
|
2023-06-02 22:31:31 +03:00
|
|
|
return (
|
|
|
|
<Modal
|
2023-08-02 15:21:14 +03:00
|
|
|
ref={ref}
|
2023-06-02 22:31:31 +03:00
|
|
|
title={false}
|
|
|
|
className="mind-popup"
|
|
|
|
overlayClassName="mind-popup-overlay"
|
|
|
|
onRequestClose={() => {
|
2023-08-02 15:21:14 +03:00
|
|
|
reset();
|
2023-06-02 22:31:31 +03:00
|
|
|
close();
|
|
|
|
|
|
|
|
if (onClose) {
|
|
|
|
onClose();
|
|
|
|
}
|
|
|
|
}}
|
|
|
|
__experimentalHideHeader
|
|
|
|
>
|
2023-08-02 15:21:14 +03:00
|
|
|
<div className="mind-popup-input">
|
|
|
|
{TOOLBAR_ICON}
|
|
|
|
<TextControl
|
|
|
|
placeholder={__('Ask AI to write anything…', 'mind')}
|
|
|
|
value={input}
|
|
|
|
onChange={(val) => {
|
|
|
|
setInput(val);
|
|
|
|
}}
|
2023-08-02 16:48:06 +03:00
|
|
|
onKeyDown={onKeyDown}
|
2023-08-02 15:21:14 +03:00
|
|
|
disabled={loading}
|
|
|
|
/>
|
|
|
|
{contextLabel ? (
|
|
|
|
<span className="mind-popup-input-context">
|
|
|
|
{contextLabel}
|
|
|
|
</span>
|
|
|
|
) : (
|
|
|
|
''
|
|
|
|
)}
|
|
|
|
</div>
|
|
|
|
{loading && <LoadingLine />}
|
2023-06-02 22:31:31 +03:00
|
|
|
<div className="mind-popup-content">
|
2023-08-02 15:21:14 +03:00
|
|
|
{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>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2023-06-02 22:31:31 +03:00
|
|
|
return (
|
2023-08-02 15:21:14 +03:00
|
|
|
<Button
|
2023-06-02 22:31:31 +03:00
|
|
|
key={data.type + data.label}
|
2023-08-02 15:21:14 +03:00
|
|
|
className="mind-popup-commands-button"
|
|
|
|
onClick={() => {
|
|
|
|
setInput(data.request);
|
|
|
|
setScreen('request');
|
|
|
|
}}
|
2023-06-02 22:31:31 +03:00
|
|
|
>
|
2023-08-02 15:21:14 +03:00
|
|
|
{data.icon || ''}
|
2023-06-02 22:31:31 +03:00
|
|
|
{data.label}
|
2023-08-02 15:21:14 +03:00
|
|
|
</Button>
|
2023-06-02 22:31:31 +03:00
|
|
|
);
|
2023-08-02 15:21:14 +03:00
|
|
|
})}
|
|
|
|
</div>
|
|
|
|
) : null}
|
2023-06-02 22:31:31 +03:00
|
|
|
|
2023-08-02 15:21:14 +03:00
|
|
|
{screen === 'request' && (
|
|
|
|
<div className="mind-popup-request">
|
|
|
|
{loading && (
|
|
|
|
<LoadingText>
|
|
|
|
{__('Waiting for AI response', 'mind')}
|
|
|
|
</LoadingText>
|
|
|
|
)}
|
|
|
|
{!loading && response && <RawHTML>{response}</RawHTML>}
|
|
|
|
{!loading && error && (
|
|
|
|
<Notice type="error">{error}</Notice>
|
|
|
|
)}
|
|
|
|
</div>
|
|
|
|
)}
|
2023-06-02 22:31:31 +03:00
|
|
|
</div>
|
2023-08-02 15:21:14 +03:00
|
|
|
{showFooter && (
|
|
|
|
<div className="mind-popup-footer">
|
|
|
|
<div className="mind-popup-footer-actions">
|
|
|
|
{input && !loading && !response && (
|
|
|
|
<Button
|
|
|
|
onClick={() => {
|
|
|
|
requestAI();
|
|
|
|
}}
|
|
|
|
>
|
|
|
|
{__('Get Answer', 'mind')} <kbd>⏎</kbd>
|
|
|
|
</Button>
|
|
|
|
)}
|
|
|
|
{response && (
|
|
|
|
<>
|
|
|
|
<Button
|
|
|
|
onClick={() => {
|
2023-08-02 16:48:06 +03:00
|
|
|
setError('');
|
|
|
|
requestAI();
|
2023-08-02 15:21:14 +03:00
|
|
|
}}
|
|
|
|
>
|
2023-08-02 16:48:06 +03:00
|
|
|
{__('Regenerate', 'mind')} <kbd>↻</kbd>
|
2023-08-02 15:21:14 +03:00
|
|
|
</Button>
|
|
|
|
<Button
|
|
|
|
onClick={() => {
|
|
|
|
copyToClipboard();
|
|
|
|
|
|
|
|
reset();
|
|
|
|
close();
|
|
|
|
|
|
|
|
if (onClose) {
|
|
|
|
onClose();
|
|
|
|
}
|
|
|
|
}}
|
|
|
|
>
|
|
|
|
{__('Copy', 'mind')}
|
|
|
|
</Button>
|
2023-08-02 16:48:06 +03:00
|
|
|
<Button onClick={onInsert}>
|
|
|
|
{__('Insert', 'mind')} <kbd>⏎</kbd>
|
2023-08-02 15:21:14 +03:00
|
|
|
</Button>
|
|
|
|
</>
|
|
|
|
)}
|
|
|
|
</div>
|
2023-06-02 22:31:31 +03:00
|
|
|
</div>
|
2023-08-02 15:21:14 +03:00
|
|
|
)}
|
2023-06-02 22:31:31 +03:00
|
|
|
</Modal>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
// .block-editor
|
|
|
|
// Insert popup renderer in editor.
|
|
|
|
domReady(() => {
|
|
|
|
// Check if popup exists already.
|
|
|
|
if (document.querySelector(`.${POPUP_CONTAINER_CLASS}`)) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
const blockEditor = document.querySelector('.block-editor');
|
|
|
|
|
|
|
|
if (!blockEditor) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
const toggleContainer = document.createElement('div');
|
|
|
|
toggleContainer.classList.add(POPUP_CONTAINER_CLASS);
|
|
|
|
|
|
|
|
blockEditor.appendChild(toggleContainer);
|
|
|
|
|
|
|
|
const root = createRoot(toggleContainer);
|
|
|
|
root.render(<Popup />);
|
|
|
|
});
|