added a lot of enhancements to popup, dropdown in toolbar and in API requests

This commit is contained in:
Nikita 2023-08-02 15:21:14 +03:00
parent 1cac92d58f
commit ce3c6c3184
53 changed files with 1784 additions and 297 deletions

View file

@ -0,0 +1,9 @@
import './style.scss';
export default function LoadingLine() {
return (
<div className="mind-popup-loading-line">
<span />
</div>
);
}

View file

@ -0,0 +1,35 @@
$move_to: 80%;
.mind-popup-loading-line {
position: relative;
margin-top: -1px;
height: 1px;
width: 100%;
// background-color: #fff;
span {
position: absolute;
display: block;
width: 60%;
left: 20%;
height: 1px;
background: linear-gradient(to right, rgba(0, 0, 0, 0%) 0%, rgba(0, 0, 0, 70%) 35%, rgba(0, 0, 0, 70%) 65%, rgba(0, 0, 0, 0%) 100%);
opacity: 0;
animation: mind-popup-loading 1.2s infinite ease-in-out;
}
}
@keyframes mind-popup-loading {
0% {
transform: scaleX(0);
opacity: 0;
}
50% {
transform: scaleX(1.3);
opacity: 1;
}
100% {
transform: scaleX(2.6);
opacity: 0;
}
}

View file

@ -0,0 +1,5 @@
import './style.scss';
export default function LoadingText(props) {
return <span className="mind-popup-loading-text">{props.children}</span>;
}

View file

@ -0,0 +1,20 @@
.mind-popup-loading-text::after {
content: "...";
animation: mind-popup-loading-text 2s infinite;
}
@keyframes mind-popup-loading-text {
0%,
100% {
content: "...";
}
25% {
content: "";
}
50% {
content: ".";
}
75% {
content: "..";
}
}

View file

@ -0,0 +1,15 @@
import './style.scss';
export default function Notice(props) {
const { type, children } = props;
return (
<div
className={`mind-popup-notice ${
type ? `mind-popup-notice-${type}` : ''
}`}
>
{children}
</div>
);
}

View file

@ -0,0 +1,13 @@
.mind-popup-notice {
padding: 10px;
border-radius: 6px;
border: 1px solid #bebebe;
background-color: #f4f4f4;
color: #434343;
}
.mind-popup-notice-error {
background-color: #fff5f5;
border-color: #eaacac;
color: #7c3939;
}

View file

@ -7,107 +7,197 @@ import './style.scss';
* WordPress dependencies
*/
import { __ } from '@wordpress/i18n';
import { createRoot } from '@wordpress/element';
import { Modal } from '@wordpress/components';
import { createRoot, useRef, useEffect, RawHTML } from '@wordpress/element';
import { Modal, Button, TextControl } from '@wordpress/components';
import { useSelect, useDispatch } from '@wordpress/data';
import { rawHandler } from '@wordpress/blocks';
import domReady from '@wordpress/dom-ready';
/**
* Internal dependencies
*/
import TOOLBAR_ICON from '../utils/icon';
import LoadingLine from './components/loading-line';
import LoadingText from './components/loading-text';
import Notice from './components/notice';
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 POPUP_CONTAINER_CLASS = 'mind-popup-container';
const prompts = [
// Base.
{
type: 'prompt',
label: __('Improve', 'mind'),
},
{
type: 'prompt',
label: __('Paraphrase', 'mind'),
},
{
type: 'prompt',
label: __('Simplify', 'mind'),
},
{
type: 'prompt',
label: __('Expand', 'mind'),
},
{
type: 'prompt',
label: __('Shorten', 'mind'),
},
// Formality.
const commands = [
{
type: 'category',
label: __('Formality', 'mind'),
label: __('Post Presets', 'mind'),
},
{
type: 'prompt',
label: __('Casual', 'mind'),
type: 'request',
label: __('Post title about…', 'mind'),
request: __('Write a post title about ', 'mind'),
icon: <PopupPostTitleAboutIcon />,
},
{
type: 'prompt',
label: __('Neutral', 'mind'),
type: 'request',
label: __('Post about…', 'mind'),
request: __('Write a blog post about ', 'mind'),
icon: <PopupPostAboutIcon />,
},
{
type: 'prompt',
label: __('Formal', 'mind'),
type: 'request',
label: __('Outline about…', 'mind'),
request: __('Write a blog post outline about ', 'mind'),
icon: <PopupOutlineAboutIcon />,
},
// Tone.
{
type: 'category',
label: __('Tone', 'mind'),
label: __('Content Presets', 'mind'),
},
{
type: 'prompt',
label: __('Friendly', 'mind'),
type: 'request',
label: __('Paragraph about…', 'mind'),
request: __('Create a paragraph about ', 'mind'),
icon: <PopupParagraphAboutIcon />,
},
{
type: 'prompt',
label: __('Professional', 'mind'),
type: 'request',
label: __('List about…', 'mind'),
request: __('Create a list about ', 'mind'),
icon: <PopupListAboutIcon />,
},
{
type: 'prompt',
label: __('Witty', 'mind'),
},
{
type: 'prompt',
label: __('Heartfelt', 'mind'),
},
{
type: 'prompt',
label: __('Educational', 'mind'),
type: 'request',
label: __('Table about…', 'mind'),
request: __('Create a table about ', 'mind'),
icon: <PopupTableAboutIcon />,
},
];
export default function Popup(props) {
const { onClose } = props;
const { close } = useDispatch('mind/popup');
const ref = useRef();
const { isOpen } = useSelect((select) => {
const { isOpen: checkIsOpen } = select('mind/popup');
const { setHighlightBlocks } = useDispatch('mind/blocks');
return { isOpen: checkIsOpen() };
const { close, reset, setInput, setScreen, setError, requestAI } =
useDispatch('mind/popup');
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(),
};
});
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;
})
);
}
}
// Set focus on Input.
useEffect(() => {
if (isOpen && ref?.current) {
focusInput();
}
}, [isOpen, ref]);
// Open request page if something is in input.
useEffect(() => {
if (screen === '' && input) {
setScreen('request');
}
}, [screen, input, setScreen]);
if (!isOpen) {
return null;
}
const showFooter = response || (input && !loading && !response);
return (
<Modal
ref={ref}
title={false}
className="mind-popup"
overlayClassName="mind-popup-overlay"
onRequestClose={() => {
reset();
close();
if (onClose) {
@ -116,37 +206,143 @@ export default function Popup(props) {
}}
__experimentalHideHeader
>
<div className="mind-popup-content">
<div className="mind-popup-prompts">
{prompts.map((data) => {
if (data.type === 'category') {
return (
<span
key={data.type + data.label}
className="mind-popup-prompts-category"
>
{data.label}
</span>
);
<div className="mind-popup-input">
{TOOLBAR_ICON}
<TextControl
placeholder={__('Ask AI to write anything…', 'mind')}
value={input}
onChange={(val) => {
setInput(val);
}}
onKeyDown={(e) => {
// Go back to starter screen.
if (
screen !== '' &&
e.key === 'Backspace' &&
!e.target.value
) {
reset();
return;
}
return (
<button
key={data.type + data.label}
className="mind-popup-prompts-button"
// Send request to AI.
if (screen === 'request' && e.key === 'Enter') {
requestAI();
}
}}
disabled={loading}
/>
{contextLabel ? (
<span className="mind-popup-input-context">
{contextLabel}
</span>
) : (
''
)}
</div>
{loading && <LoadingLine />}
<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');
focusInput();
}}
>
{data.icon || ''}
{data.label}
</Button>
);
})}
</div>
) : null}
{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>
)}
</div>
{showFooter && (
<div className="mind-popup-footer">
<div className="mind-popup-footer-actions">
{input && !loading && !response && (
<Button
onClick={() => {
requestAI();
}}
>
{data.label}
</button>
);
})}
{__('Get Answer', 'mind')} <kbd></kbd>
</Button>
)}
{response && (
<>
<Button
onClick={() => {
insertResponse();
reset();
close();
if (onClose) {
onClose();
}
}}
>
{__('Insert', 'mind')} <kbd></kbd>
</Button>
<Button
onClick={() => {
copyToClipboard();
reset();
close();
if (onClose) {
onClose();
}
}}
>
{__('Copy', 'mind')}
</Button>
<Button
onClick={() => {
setError('');
requestAI();
}}
>
{__('Regenerate', 'mind')} <kbd></kbd>
</Button>
</>
)}
</div>
</div>
</div>
<div className="mind-popup-footer">
<div className="mind-popup-footer-logo">
{TOOLBAR_ICON}
{__('Mind', '@@text_domain')}
</div>
</div>
)}
</Modal>
);
}

View file

@ -1,14 +1,18 @@
$padding: 10px;
// Popup.
.mind-popup {
position: relative;
top: 15%;
margin-top: 0;
width: 750px;
max-height: 380px;
max-height: clamp(0px, 440px, 75vh);
border-radius: 10px;
color: #000;
.components-modal__content {
padding: 0;
overflow: hidden;
> div {
display: flex;
@ -25,42 +29,164 @@
.mind-popup-content {
overflow: auto;
flex: 1;
padding: 10px;
}
.mind-popup-footer {
padding: 10px;
background-color: #f6f6f6;
border-top: 1px solid #e8e7e7;
}
.mind-popup-footer-logo {
display: flex;
gap: 6px;
font-weight: 500;
color: var(--mind-brand-color);
}
padding: $padding $padding * 2;
.mind-popup-prompts {
display: flex;
flex-direction: column;
.mind-popup-prompts-category {
font-weight: 500;
padding: 10px;
padding-top: 20px;
color: #b5b1b1;
&:empty,
&:has(.mind-popup-request:empty) {
padding: 0;
margin-bottom: -1px;
}
.mind-popup-prompts-button {
.mind-popup-request {
ol,
ul {
list-style: auto;
padding-left: 15px;
}
table {
border-collapse: collapse;
width: 100%;
td,
th {
border: 1px solid;
padding: 0.5em;
white-space: pre-wrap;
min-width: 1px;
}
}
}
}
.mind-popup-footer {
display: flex;
justify-content: space-between;
align-items: center;
padding: $padding $padding * 2;
background-color: #f9f9f9;
border-top: 1px solid #e8e7e7;
}
.mind-popup-footer-actions {
display: flex;
margin: -5px -15px;
margin-left: auto;
gap: 5px;
button {
display: flex;
gap: 8px;
height: 28px;
border-radius: 5px;
font-weight: 500;
&:hover,
&:focus {
background-color: rgba(0, 0, 0, 5%);
color: #121212;
}
kbd {
font: inherit;
font-weight: 400;
border-radius: 3px;
padding: 3px 4px;
margin-right: -8px;
color: rgba(0, 0, 0, 50%);
background-color: rgba(0, 0, 0, 8%);
}
}
}
// Input.
.mind-popup-input {
display: flex;
align-items: center;
margin-bottom: 0;
border-bottom: 1px solid #e8e7e7;
> svg {
position: absolute;
left: $padding * 2;
pointer-events: none;
}
> .components-base-control {
flex: 1;
}
.components-base-control__field {
margin-bottom: 0;
}
.components-base-control__field input {
border: none !important;
box-shadow: none !important;
padding: $padding * 2;
padding-left: $padding * 5;
font-size: 1.15em;
color: #151515;
&::placeholder {
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%);
}
}
// Prompts.
.mind-popup-commands {
display: flex;
flex-direction: column;
margin-left: -$padding;
margin-right: -$padding;
.mind-popup-commands-category {
position: relative;
padding: $padding;
padding-top: 28px;
color: #7f7f7f;
&:first-child {
padding-top: $padding;
}
&:not(:first-child)::before {
content: "";
display: block;
position: absolute;
top: 8px;
left: -10px;
right: -10px;
border-top: 1px solid #e8e7e7;
}
}
.mind-popup-commands-button {
display: flex;
align-items: center;
background-color: transparent;
color: #000;
border: none;
text-align: left;
padding: 10px;
padding: $padding;
border-radius: 5px;
height: auto;
min-height: 40px;
&:hover,
&:focus {
background-color: rgba(#000, 5%);
}
> svg {
margin-right: 10px;
}
}
}