mirror of
https://github.com/WenPai-org/wpmind.git
synced 2025-08-07 22:47:49 +08:00
added a lot of enhancements to popup, dropdown in toolbar and in API requests
This commit is contained in:
parent
1cac92d58f
commit
ce3c6c3184
53 changed files with 1784 additions and 297 deletions
9
src/popup/components/loading-line/index.js
Normal file
9
src/popup/components/loading-line/index.js
Normal file
|
@ -0,0 +1,9 @@
|
|||
import './style.scss';
|
||||
|
||||
export default function LoadingLine() {
|
||||
return (
|
||||
<div className="mind-popup-loading-line">
|
||||
<span />
|
||||
</div>
|
||||
);
|
||||
}
|
35
src/popup/components/loading-line/style.scss
Normal file
35
src/popup/components/loading-line/style.scss
Normal 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;
|
||||
}
|
||||
}
|
5
src/popup/components/loading-text/index.js
Normal file
5
src/popup/components/loading-text/index.js
Normal file
|
@ -0,0 +1,5 @@
|
|||
import './style.scss';
|
||||
|
||||
export default function LoadingText(props) {
|
||||
return <span className="mind-popup-loading-text">{props.children}</span>;
|
||||
}
|
20
src/popup/components/loading-text/style.scss
Normal file
20
src/popup/components/loading-text/style.scss
Normal 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: "..";
|
||||
}
|
||||
}
|
15
src/popup/components/notice/index.js
Normal file
15
src/popup/components/notice/index.js
Normal 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>
|
||||
);
|
||||
}
|
13
src/popup/components/notice/style.scss
Normal file
13
src/popup/components/notice/style.scss
Normal 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;
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue