├── packages ├── eslint-import-resolver │ ├── README.md │ └── src │ │ ├── test │ │ └── resolve.ts │ │ └── index.js ├── editor │ ├── src │ │ ├── style.css │ │ ├── @types │ │ │ └── wordpress__block-editor │ │ │ │ ├── store │ │ │ │ ├── defaults.d.ts │ │ │ │ ├── index.d.ts │ │ │ │ ├── actions.d.ts │ │ │ │ └── selectors.d.ts │ │ │ │ └── index.d.ts │ │ ├── index.tsx │ │ ├── formats.ts │ │ ├── blockControls │ │ │ ├── index.tsx │ │ │ ├── imageControls.tsx │ │ │ ├── autocomplete.tsx │ │ │ └── paragraphControls.tsx │ │ ├── workers │ │ │ └── altText.ts │ │ ├── utils.ts │ │ └── commands.tsx │ └── tsconfig.json ├── summarize-button │ ├── src │ │ ├── @types │ │ │ └── wordpress__block-editor │ │ │ │ ├── store │ │ │ │ ├── defaults.d.ts │ │ │ │ ├── index.d.ts │ │ │ │ ├── actions.d.ts │ │ │ │ └── selectors.d.ts │ │ │ │ └── index.d.ts │ │ ├── edit.tsx │ │ ├── block.json │ │ ├── index.tsx │ │ ├── style.css │ │ └── view.ts │ └── tsconfig.json └── translate-button │ ├── tsconfig.json │ └── src │ ├── style.css │ ├── edit.tsx │ ├── block.json │ ├── index.tsx │ └── view.ts ├── tsconfig.json ├── ai-experiments.php ├── patches ├── @wordpress+editor+14.26.0.patch ├── @wordpress+components+29.3.0.patch ├── @huggingface+transformers+3.6.1.patch └── react-autosize-textarea+7.1.0.patch ├── blueprints └── playground.json ├── tsconfig.shared.json ├── composer.json ├── webpack.config.js ├── README.md ├── LICENSE └── inc └── functions.php /packages/eslint-import-resolver/README.md: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /packages/editor/src/style.css: -------------------------------------------------------------------------------- 1 | .aiwp-autocomplete { 2 | color: green; 3 | } 4 | -------------------------------------------------------------------------------- /packages/editor/src/@types/wordpress__block-editor/store/defaults.d.ts: -------------------------------------------------------------------------------- 1 | export const SETTINGS_DEFAULTS: Record< string, unknown >; 2 | -------------------------------------------------------------------------------- /packages/editor/src/index.tsx: -------------------------------------------------------------------------------- 1 | import './blockControls'; 2 | import './commands'; 3 | import './formats'; 4 | 5 | import './style.css'; 6 | -------------------------------------------------------------------------------- /packages/summarize-button/src/@types/wordpress__block-editor/store/defaults.d.ts: -------------------------------------------------------------------------------- 1 | export const SETTINGS_DEFAULTS: Record< string, unknown >; 2 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "references": [ 3 | { 4 | "path": "packages/editor" 5 | }, 6 | { 7 | "path": "packages/summarize-button" 8 | } 9 | ], 10 | "files": [] 11 | } 12 | -------------------------------------------------------------------------------- /packages/editor/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.shared.json", 3 | "compilerOptions": { 4 | "rootDir": "src", 5 | "declarationDir": "dist-types" 6 | }, 7 | "references": [], 8 | "include": [ "src/**/*" ] 9 | } 10 | -------------------------------------------------------------------------------- /packages/summarize-button/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.shared.json", 3 | "compilerOptions": { 4 | "rootDir": "src", 5 | "declarationDir": "dist-types" 6 | }, 7 | "references": [], 8 | "include": [ "src/**/*" ] 9 | } 10 | -------------------------------------------------------------------------------- /packages/translate-button/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.shared.json", 3 | "compilerOptions": { 4 | "rootDir": "src", 5 | "declarationDir": "dist-types" 6 | }, 7 | "references": [], 8 | "include": [ "src/**/*" ] 9 | } 10 | -------------------------------------------------------------------------------- /packages/editor/src/@types/wordpress__block-editor/store/index.d.ts: -------------------------------------------------------------------------------- 1 | // This is only used internally by `@wordpress/editor`. Not worth the effort of typing this. 2 | export const storeConfig: { 3 | reducer: any; 4 | selectors: any; 5 | actions: any; 6 | controls: any; 7 | }; 8 | -------------------------------------------------------------------------------- /packages/summarize-button/src/@types/wordpress__block-editor/store/index.d.ts: -------------------------------------------------------------------------------- 1 | // This is only used internally by `@wordpress/editor`. Not worth the effort of typing this. 2 | export const storeConfig: { 3 | reducer: any; 4 | selectors: any; 5 | actions: any; 6 | controls: any; 7 | }; 8 | -------------------------------------------------------------------------------- /packages/eslint-import-resolver/src/test/resolve.ts: -------------------------------------------------------------------------------- 1 | import { resolve } from '../'; 2 | 3 | describe( 'resolve', () => { 4 | it( 'resolves file in monorepo package', () => { 5 | const result = resolve( '@ai-experiments/upload-media', 'index.ts' ); 6 | expect( result.found ).toBe( true ); 7 | } ); 8 | } ); 9 | -------------------------------------------------------------------------------- /packages/editor/src/formats.ts: -------------------------------------------------------------------------------- 1 | import { registerFormatType } from '@wordpress/rich-text'; 2 | 3 | registerFormatType( 'ai-experiments/autocomplete', { 4 | name: 'ai-experiments/autocomplete', 5 | title: 'Autocomplete', 6 | tagName: 'samp', 7 | className: 'aiwp-autocomplete', 8 | interactive: false, 9 | edit: () => null, 10 | object: false, 11 | } ); 12 | -------------------------------------------------------------------------------- /packages/translate-button/src/style.css: -------------------------------------------------------------------------------- 1 | .ai-translation-button svg { 2 | fill: currentColor; 3 | } 4 | 5 | .ai-translation-button { 6 | display: none !important; 7 | } 8 | 9 | .ai-translation-button.visible { 10 | display: flex !important; 11 | } 12 | 13 | .ai-translation-text { 14 | display: none; 15 | } 16 | 17 | .ai-translation-text.visible { 18 | display: block; 19 | } 20 | -------------------------------------------------------------------------------- /packages/translate-button/src/edit.tsx: -------------------------------------------------------------------------------- 1 | import { __ } from '@wordpress/i18n'; 2 | import { useBlockProps } from '@wordpress/block-editor'; 3 | 4 | export default function Edit( { 5 | // @ts-ignore 6 | __unstableLayoutClassNames: layoutClassNames, 7 | } ) { 8 | const blockProps = useBlockProps( { className: layoutClassNames } ); 9 | return ( 10 |
11 | 14 |
15 | ); 16 | } 17 | -------------------------------------------------------------------------------- /packages/summarize-button/src/edit.tsx: -------------------------------------------------------------------------------- 1 | import { __ } from '@wordpress/i18n'; 2 | import { useBlockProps } from '@wordpress/block-editor'; 3 | 4 | export default function Edit( { 5 | // @ts-ignore 6 | __unstableLayoutClassNames: layoutClassNames, 7 | } ) { 8 | const blockProps = useBlockProps( { className: layoutClassNames } ); 9 | return ( 10 |
11 | 14 |
15 | ); 16 | } 17 | -------------------------------------------------------------------------------- /packages/translate-button/src/block.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://schemas.wp.org/trunk/block.json", 3 | "apiVersion": 3, 4 | "name": "ai-experiments/translate-button", 5 | "version": "0.0.1", 6 | "title": "Translate", 7 | "category": "widgets", 8 | "description": "Block that allows translating a comment", 9 | "supports": { 10 | "html": false, 11 | "layout": true, 12 | "interactivity": true 13 | }, 14 | "textdomain": "translate-button", 15 | "blockHooks": { 16 | "core/post-content": "before", 17 | "core/comment-content": "after" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /packages/summarize-button/src/block.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://schemas.wp.org/trunk/block.json", 3 | "apiVersion": 3, 4 | "name": "ai-experiments/summarize-button", 5 | "version": "0.0.1", 6 | "title": "Summarize", 7 | "category": "widgets", 8 | "description": "Block that allows summarizing the post content", 9 | "supports": { 10 | "html": false, 11 | "layout": true, 12 | "interactivity": true 13 | }, 14 | "textdomain": "ai-experiments", 15 | "blockHooks": { 16 | "core/post-content": "before", 17 | "core/comment-template": "before" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /ai-experiments.php: -------------------------------------------------------------------------------- 1 | ( 9 | 16 | 17 | 18 | ); 19 | 20 | // @ts-ignore 21 | registerBlockType( metadata.name, { 22 | ...metadata, 23 | edit: Edit, 24 | icon: SparkIcon, 25 | } ); 26 | -------------------------------------------------------------------------------- /packages/translate-button/src/index.tsx: -------------------------------------------------------------------------------- 1 | import { registerBlockType } from '@wordpress/blocks'; 2 | 3 | import './style.css'; 4 | 5 | import Edit from './edit'; 6 | import metadata from './block.json'; 7 | 8 | const SparkIcon = () => ( 9 | 16 | 17 | 18 | ); 19 | 20 | // @ts-ignore 21 | registerBlockType( metadata.name, { 22 | ...metadata, 23 | edit: Edit, 24 | icon: SparkIcon, 25 | } ); 26 | -------------------------------------------------------------------------------- /packages/summarize-button/src/style.css: -------------------------------------------------------------------------------- 1 | .ai-summary-button svg { 2 | fill: currentColor; 3 | } 4 | 5 | .ai-summary-popover { 6 | top: calc(anchor(bottom) + 20px); 7 | left: 50vw; 8 | translate: -50% 0; 9 | 10 | background: #eee; 11 | border: 1px solid #333; 12 | position: relative; 13 | margin: 0 !important; 14 | padding: 40px 40px 20px 20px; 15 | min-width: 50vw; 16 | min-height: 20vh; 17 | } 18 | 19 | .ai-summary-popover::backdrop { 20 | } 21 | 22 | .ai-summary-popover-close { 23 | position: absolute; 24 | top: 20px; 25 | right: 20px; 26 | cursor: pointer; 27 | z-index: 5000000; 28 | min-width: 40px; 29 | align-items: center; 30 | justify-content: center; 31 | } 32 | 33 | .ai-summary-popover p { 34 | margin: 0 !important; 35 | } 36 | -------------------------------------------------------------------------------- /patches/@wordpress+editor+14.26.0.patch: -------------------------------------------------------------------------------- 1 | diff --git a/node_modules/@wordpress/editor/build-types/store/actions.d.ts b/node_modules/@wordpress/editor/build-types/store/actions.d.ts 2 | index ec245fc..c0da05e 100644 3 | --- a/node_modules/@wordpress/editor/build-types/store/actions.d.ts 4 | +++ b/node_modules/@wordpress/editor/build-types/store/actions.d.ts 5 | @@ -217,7 +217,7 @@ export function autosave({ local, ...options }?: { 6 | select: any; 7 | dispatch: any; 8 | }) => Promise; 9 | -export function __unstableSaveForPreview({ forceIsAutosaveable }?: {}): ({ select, dispatch }: { 10 | +export function __unstableSaveForPreview({ forceIsAutosaveable }?: { forceIsAutosaveable?: boolean }): ({ select, dispatch }: { 11 | select: any; 12 | dispatch: any; 13 | }) => Promise; 14 | -------------------------------------------------------------------------------- /blueprints/playground.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://playground.wordpress.net/blueprint-schema.json", 3 | "landingPage": "/wp-admin/post-new.php", 4 | "preferredVersions": { 5 | "php": "latest", 6 | "wp": "latest" 7 | }, 8 | "phpExtensionBundles": [ "kitchen-sink" ], 9 | "steps": [ 10 | { 11 | "step": "installPlugin", 12 | "pluginZipFile": { 13 | "resource": "url", 14 | "url": "https://swissspidy.github.io/ai-experiments/nightly.zip" 15 | } 16 | }, 17 | { 18 | "step": "activatePlugin", 19 | "pluginName": "AI Experiments", 20 | "pluginPath": "/wordpress/wp-content/plugins/ai-experiments" 21 | }, 22 | { 23 | "step": "login", 24 | "username": "admin", 25 | "password": "password" 26 | }, 27 | { 28 | "step": "defineWpConfigConsts", 29 | "consts": { 30 | "WPAI_IS_PLAYGROUND": true 31 | } 32 | } 33 | ] 34 | } 35 | -------------------------------------------------------------------------------- /packages/editor/src/blockControls/index.tsx: -------------------------------------------------------------------------------- 1 | import { createHigherOrderComponent } from '@wordpress/compose'; 2 | import { addFilter } from '@wordpress/hooks'; 3 | 4 | import { ParagraphControls } from './paragraphControls'; 5 | import { ImageControls } from './imageControls'; 6 | import { Autocomplete } from './autocomplete'; 7 | 8 | const addAiControls = createHigherOrderComponent( 9 | ( BlockEdit ) => ( props ) => { 10 | if ( props.name === 'core/paragraph' ) { 11 | return ( 12 | <> 13 | 14 | 15 | 16 | 17 | ); 18 | } 19 | 20 | if ( props.name === 'core/image' ) { 21 | return ( 22 | <> 23 | 24 | 25 | 26 | ); 27 | } 28 | 29 | return ; 30 | }, 31 | 'withAiControls' 32 | ); 33 | 34 | addFilter( 35 | 'editor.BlockEdit', 36 | 'ai-experiments/add-ai-controls', 37 | addAiControls 38 | ); 39 | -------------------------------------------------------------------------------- /patches/@wordpress+components+29.3.0.patch: -------------------------------------------------------------------------------- 1 | diff --git a/node_modules/@wordpress/components/build-types/card/card-divider/hook.d.ts b/node_modules/@wordpress/components/build-types/card/card-divider/hook.d.ts 2 | index dc94460..c886f2c 100644 3 | --- a/node_modules/@wordpress/components/build-types/card/card-divider/hook.d.ts 4 | +++ b/node_modules/@wordpress/components/build-types/card/card-divider/hook.d.ts 5 | @@ -270,7 +270,7 @@ export declare function useCardDivider(props: WordPressComponentProps { 25 | const resolve = ( sourcePath ) => 26 | nodeResolver.resolve( sourcePath, file, { 27 | ...config, 28 | extensions: [ '.tsx', '.ts', '.mjs', '.js', '.json' ], 29 | } ); 30 | 31 | if ( source.startsWith( '@ai-experiments/' ) ) { 32 | const packageName = source.slice( '@ai-experiments/'.length ); 33 | 34 | const result = resolve( path.join( PACKAGES_DIR, packageName ) ); 35 | 36 | if ( result.found ) { 37 | return result; 38 | } 39 | 40 | return resolve( path.join( PACKAGES_DIR, `${ packageName }/src/` ) ); 41 | } 42 | 43 | return resolve( source ); 44 | }; 45 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "swissspidy/ai-experiments", 3 | "description": "WordPress AI experiments", 4 | "license": "Apache-2.0", 5 | "type": "wordpress-plugin", 6 | "authors": [ 7 | { 8 | "name": "Pascal Birchler", 9 | "email": "swissspidy@chat.wordpress.org", 10 | "homepage": "https://pascalbirchler.com", 11 | "role": "Developer" 12 | } 13 | ], 14 | "require": { 15 | "php": ">=8.0", 16 | "ext-json": "*" 17 | }, 18 | "require-dev": { 19 | "dealerdirect/phpcodesniffer-composer-installer": "^1.0.0", 20 | "php-stubs/wordpress-tests-stubs": "dev-master", 21 | "phpcompatibility/phpcompatibility-wp": "^2.0", 22 | "phpstan/extension-installer": "^1.3", 23 | "roave/security-advisories": "dev-latest", 24 | "szepeviktor/phpstan-wordpress": "^v2.0.1", 25 | "wp-coding-standards/wpcs": "^3.0.1", 26 | "yoast/phpunit-polyfills": "^3.0.0" 27 | }, 28 | "minimum-stability": "dev", 29 | "config": { 30 | "allow-plugins": { 31 | "dealerdirect/phpcodesniffer-composer-installer": true, 32 | "phpstan/extension-installer": true 33 | }, 34 | "platform": { 35 | "php": "8.0.28" 36 | } 37 | }, 38 | "scripts": { 39 | "format": "vendor/bin/phpcbf --report-summary --report-source .", 40 | "lint": "vendor/bin/phpcs --report-summary --report-source .", 41 | "phpstan": "phpstan analyse --memory-limit=2048M", 42 | "test": "vendor/bin/phpunit", 43 | "test:multisite": "vendor/bin/phpunit -c phpunit-multisite.xml.dist" 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /patches/@huggingface+transformers+3.6.1.patch: -------------------------------------------------------------------------------- 1 | diff --git a/node_modules/@huggingface/transformers/types/models/auto/processing_auto.d.ts b/node_modules/@huggingface/transformers/types/models/auto/processing_auto.d.ts 2 | index b8e76a2..9289cca 100644 3 | --- a/node_modules/@huggingface/transformers/types/models/auto/processing_auto.d.ts 4 | +++ b/node_modules/@huggingface/transformers/types/models/auto/processing_auto.d.ts 5 | @@ -1,3 +1,4 @@ 6 | +import type { PretrainedProcessorOptions } from '../../base/processing_utils'; 7 | /** 8 | * Helper class which is used to instantiate pretrained processors with the `from_pretrained` function. 9 | * The chosen processor class is determined by the type specified in the processor config. 10 | diff --git a/node_modules/@huggingface/transformers/types/models/mgp_str/processing_mgp_str.d.ts b/node_modules/@huggingface/transformers/types/models/mgp_str/processing_mgp_str.d.ts 11 | index 4e40812..b830b6f 100644 12 | --- a/node_modules/@huggingface/transformers/types/models/mgp_str/processing_mgp_str.d.ts 13 | +++ b/node_modules/@huggingface/transformers/types/models/mgp_str/processing_mgp_str.d.ts 14 | @@ -49,6 +49,7 @@ export class MgpstrProcessor extends Processor { 15 | * - bpe_preds: The list of BPE decoded sentences. 16 | * - wp_preds: The list of wp decoded sentences. 17 | */ 18 | + // @ts-ignore 19 | batch_decode([char_logits, bpe_logits, wp_logits]: import("../../utils/tensor.js").Tensor[]): { 20 | generated_text: string[]; 21 | scores: number[]; 22 | -------------------------------------------------------------------------------- /packages/editor/src/workers/altText.ts: -------------------------------------------------------------------------------- 1 | import { 2 | env, 3 | Florence2ForConditionalGeneration, 4 | AutoProcessor, 5 | AutoTokenizer, 6 | RawImage, 7 | type Tensor, 8 | PreTrainedModel, 9 | Processor, 10 | PreTrainedTokenizer, 11 | } from '@huggingface/transformers'; 12 | 13 | let model: PreTrainedModel; 14 | let processor: Processor; 15 | let tokenizer: PreTrainedTokenizer; 16 | 17 | async function loadModels() { 18 | env.allowLocalModels = false; 19 | env.allowRemoteModels = true; 20 | if ( env.backends.onnx.wasm ) { 21 | env.backends.onnx.wasm.proxy = false; 22 | } 23 | 24 | const modelId = 'onnx-community/Florence-2-base-ft'; 25 | model = await Florence2ForConditionalGeneration.from_pretrained( modelId, { 26 | dtype: 'fp32', 27 | device: 'webgpu', 28 | } ); 29 | processor = await AutoProcessor.from_pretrained( modelId ); 30 | tokenizer = await AutoTokenizer.from_pretrained( modelId ); 31 | } 32 | 33 | export async function runTask( 34 | url: string, 35 | task: 36 | | '' 37 | | '' 38 | | '' = '' 39 | ) { 40 | if ( ! processor || ! model || ! tokenizer ) { 41 | await loadModels(); 42 | } 43 | 44 | if ( ! processor || ! model || ! tokenizer ) { 45 | return; 46 | } 47 | 48 | // Load image and prepare vision inputs 49 | const image = await RawImage.fromURL( url ); 50 | // @ts-ignore 51 | const visionInputs = await processor( image ); 52 | 53 | // @ts-ignore 54 | const prompts = processor.construct_prompts( task ); 55 | // @ts-ignore 56 | const textInputs = tokenizer( prompts ); 57 | 58 | // Generate text 59 | // @ts-ignore 60 | const generatedIds = ( await model.generate( { 61 | ...textInputs, 62 | ...visionInputs, 63 | max_new_tokens: 100, 64 | } ) ) as Tensor; 65 | 66 | // Decode generated text 67 | // @ts-ignore 68 | const generatedText = tokenizer.batch_decode( generatedIds, { 69 | skip_special_tokens: false, 70 | } )[ 0 ]; 71 | 72 | // Post-process the generated text 73 | // @ts-ignore 74 | const result = processor.post_process_generation( 75 | generatedText, 76 | task, 77 | image.size 78 | ); 79 | 80 | return result[ task ]; 81 | } 82 | -------------------------------------------------------------------------------- /packages/summarize-button/src/view.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * WordPress dependencies 3 | */ 4 | import { store, getContext } from '@wordpress/interactivity'; 5 | 6 | async function summarizePostContent() { 7 | const postContent = 8 | document.querySelector( '.wp-block-post-content' )?.textContent || ''; 9 | const summarizer = await Summarizer.create( { 10 | sharedContext: 'A blog post', 11 | format: 'plain-text', 12 | } ); 13 | return summarizer.summarize( postContent, { 14 | context: 'Avoid any toxic language and be as constructive as possible.', 15 | } ); 16 | } 17 | 18 | async function summarizeComments() { 19 | let allComments = ''; 20 | document 21 | .querySelectorAll( '.wp-block-comment-content' ) 22 | .forEach( ( node ) => ( allComments += node.textContent + '\n\n' ) ); 23 | const summarizer = await Summarizer.create( { 24 | sharedContext: 'A list of user-generated comments on a blog post', 25 | format: 'plain-text', 26 | type: 'tldr', 27 | } ); 28 | return summarizer.summarize( allComments, { 29 | context: 'Avoid any toxic language and be as constructive as possible.', 30 | } ); 31 | } 32 | 33 | type BlockContext = { 34 | summaryContext: string; 35 | summary: string; 36 | isOpen: boolean; 37 | isLoading: boolean; 38 | buttonText: string; 39 | }; 40 | 41 | store( 42 | 'ai-experiments/summarize-button', 43 | { 44 | state: { 45 | get isLoading(): boolean { 46 | const context = getContext< BlockContext >(); 47 | return context.isOpen && ! context.summary; 48 | }, 49 | }, 50 | actions: { 51 | *generateSummary() { 52 | const context = getContext< BlockContext >(); 53 | 54 | context.isOpen = ! context.isOpen; 55 | 56 | if ( ! context.summary ) { 57 | context.isLoading = true; 58 | context.buttonText = 'Loading...'; 59 | 60 | if ( 'post' === context.summaryContext ) { 61 | context.summary = yield summarizePostContent(); 62 | } else { 63 | context.summary = yield summarizeComments(); 64 | } 65 | 66 | context.buttonText = 'Read AI-generated summary'; 67 | context.isLoading = false; 68 | } 69 | }, 70 | }, 71 | callbacks: { 72 | closeSummary: () => { 73 | const context = getContext< BlockContext >(); 74 | 75 | context.isOpen = ! context.isOpen; 76 | context.buttonText = 'Read AI-generated summary'; 77 | }, 78 | }, 79 | }, 80 | { lock: true } 81 | ); 82 | -------------------------------------------------------------------------------- /packages/editor/src/blockControls/imageControls.tsx: -------------------------------------------------------------------------------- 1 | import { createWorkerFactory } from '@shopify/web-worker'; 2 | 3 | import { __ } from '@wordpress/i18n'; 4 | import { BlockControls } from '@wordpress/block-editor'; 5 | import { ToolbarDropdownMenu } from '@wordpress/components'; 6 | import { useState } from '@wordpress/element'; 7 | 8 | const createAltTextWorker = createWorkerFactory( 9 | () => import( /* webpackChunkName: 'foo' */ '../workers/altText' ) 10 | ); 11 | 12 | const altTextWorker = createAltTextWorker(); 13 | 14 | const photoSparkIcon = () => ( 15 | 22 | 23 | 24 | ); 25 | 26 | // @ts-ignore 27 | export function ImageControls( { attributes, setAttributes } ) { 28 | const [ inProgress, setInProgress ] = useState( false ); 29 | 30 | if ( ! attributes.url ) { 31 | return null; 32 | } 33 | 34 | const controls = [ 35 | { 36 | title: __( 'Write caption', 'ai-experiments' ), 37 | onClick: async () => { 38 | setInProgress( true ); 39 | 40 | const task = ''; 41 | const result = await altTextWorker.runTask( 42 | attributes.url, 43 | task 44 | ); 45 | 46 | setAttributes( { caption: result } ); 47 | 48 | setInProgress( false ); 49 | }, 50 | role: 'menuitemradio', 51 | icon: undefined, 52 | }, 53 | { 54 | title: __( 'Write alternative text', 'ai-experiments' ), 55 | onClick: async () => { 56 | setInProgress( true ); 57 | 58 | const task = ''; 59 | const result = await altTextWorker.runTask( 60 | attributes.url, 61 | task 62 | ); 63 | 64 | setAttributes( { alt: result } ); 65 | 66 | setInProgress( false ); 67 | }, 68 | role: 'menuitemradio', 69 | icon: undefined, 70 | }, 71 | ]; 72 | return ( 73 | 74 | 82 | 83 | ); 84 | } 85 | -------------------------------------------------------------------------------- /packages/editor/src/@types/wordpress__block-editor/index.d.ts: -------------------------------------------------------------------------------- 1 | declare module '@wordpress/block-editor' { 2 | import type { 3 | ReduxStoreConfig, 4 | StoreDescriptor, 5 | } from '@wordpress/data/build-types/types'; 6 | 7 | import type { 8 | ComponentType, 9 | FC, 10 | MouseEventHandler, 11 | ReactFragment, 12 | ReactNode, 13 | ComponentProps, 14 | } from 'react'; 15 | import type { Slot, Toolbar } from '@wordpress/components'; 16 | 17 | const store: { 18 | name: 'core/block-editor'; 19 | } & StoreDescriptor< 20 | ReduxStoreConfig< 21 | unknown, 22 | typeof import('./store/actions'), 23 | typeof import('./store/selectors') 24 | > 25 | >; 26 | 27 | import type { Ref, RefCallback } from 'react'; 28 | 29 | interface Reserved { 30 | id: string; 31 | role: 'document'; 32 | tabIndex: 0; 33 | 'aria-label': string; 34 | 'data-block': string; 35 | 'data-type': string; 36 | 'data-title': string; 37 | } 38 | 39 | interface Merged { 40 | className: string; 41 | style: Record< string, unknown >; 42 | ref: RefCallback< unknown >; 43 | } 44 | 45 | interface UseBlockProps { 46 | < Props extends Record< string, unknown > >( 47 | props?: Props & { 48 | [ K in keyof Props ]: K extends keyof Reserved 49 | ? never 50 | : Props[ K ]; 51 | } & { ref?: Ref< unknown > } 52 | ): Omit< Props, 'ref' > & Merged & Reserved; 53 | 54 | save: ( 55 | props?: Record< string, unknown > 56 | ) => Record< string, unknown >; 57 | } 58 | 59 | const useBlockProps: UseBlockProps; 60 | 61 | interface WarningProps { 62 | actions?: ReactFragment | undefined; 63 | children: ReactNode; 64 | className?: string | undefined; 65 | secondaryActions?: 66 | | Array< { 67 | title: ReactNode; 68 | onClick: MouseEventHandler< HTMLButtonElement >; 69 | } > 70 | | undefined; 71 | } 72 | 73 | const Warning: ComponentType< WarningProps >; 74 | 75 | interface BlockControlsProps extends ComponentProps< typeof Toolbar > { 76 | children: ReactNode; 77 | group: string; 78 | } 79 | 80 | const BlockControls: { 81 | ( props: Partial< BlockControlsProps > ): JSX.Element; 82 | Slot: FC< Omit< ComponentProps< typeof Slot >, 'name' > >; 83 | }; 84 | 85 | interface InspectorControlsProps { 86 | children: ReactNode; 87 | } 88 | 89 | const InspectorControls: ( props: InspectorControlsProps ) => JSX.Element; 90 | 91 | interface BlockTitleProps { 92 | clientId: string; 93 | maximumLength?: number; 94 | context?: string; 95 | } 96 | 97 | const BlockTitle: ( props: BlockTitleProps ) => JSX.Element; 98 | } 99 | -------------------------------------------------------------------------------- /packages/summarize-button/src/@types/wordpress__block-editor/index.d.ts: -------------------------------------------------------------------------------- 1 | declare module '@wordpress/block-editor' { 2 | import type { 3 | ReduxStoreConfig, 4 | StoreDescriptor, 5 | } from '@wordpress/data/build-types/types'; 6 | 7 | import type { 8 | ComponentType, 9 | FC, 10 | MouseEventHandler, 11 | ReactFragment, 12 | ReactNode, 13 | ComponentProps, 14 | } from 'react'; 15 | import type { Slot, Toolbar } from '@wordpress/components'; 16 | 17 | const store: { 18 | name: 'core/block-editor'; 19 | } & StoreDescriptor< 20 | ReduxStoreConfig< 21 | unknown, 22 | typeof import('./store/actions'), 23 | typeof import('./store/selectors') 24 | > 25 | >; 26 | 27 | import type { Ref, RefCallback } from 'react'; 28 | 29 | interface Reserved { 30 | id: string; 31 | role: 'document'; 32 | tabIndex: 0; 33 | 'aria-label': string; 34 | 'data-block': string; 35 | 'data-type': string; 36 | 'data-title': string; 37 | } 38 | 39 | interface Merged { 40 | className: string; 41 | style: Record< string, unknown >; 42 | ref: RefCallback< unknown >; 43 | } 44 | 45 | interface UseBlockProps { 46 | < Props extends Record< string, unknown > >( 47 | props?: Props & { 48 | [ K in keyof Props ]: K extends keyof Reserved 49 | ? never 50 | : Props[ K ]; 51 | } & { ref?: Ref< unknown > } 52 | ): Omit< Props, 'ref' > & Merged & Reserved; 53 | 54 | save: ( 55 | props?: Record< string, unknown > 56 | ) => Record< string, unknown >; 57 | } 58 | 59 | const useBlockProps: UseBlockProps; 60 | 61 | interface WarningProps { 62 | actions?: ReactFragment | undefined; 63 | children: ReactNode; 64 | className?: string | undefined; 65 | secondaryActions?: 66 | | Array< { 67 | title: ReactNode; 68 | onClick: MouseEventHandler< HTMLButtonElement >; 69 | } > 70 | | undefined; 71 | } 72 | 73 | const Warning: ComponentType< WarningProps >; 74 | 75 | interface BlockControlsProps extends ComponentProps< typeof Toolbar > { 76 | children: ReactNode; 77 | group: string; 78 | } 79 | 80 | const BlockControls: { 81 | ( props: Partial< BlockControlsProps > ): JSX.Element; 82 | Slot: FC< Omit< ComponentProps< typeof Slot >, 'name' > >; 83 | }; 84 | 85 | interface InspectorControlsProps { 86 | children: ReactNode; 87 | } 88 | 89 | const InspectorControls: ( props: InspectorControlsProps ) => JSX.Element; 90 | 91 | interface BlockTitleProps { 92 | clientId: string; 93 | maximumLength?: number; 94 | context?: string; 95 | } 96 | 97 | const BlockTitle: ( props: BlockTitleProps ) => JSX.Element; 98 | } 99 | -------------------------------------------------------------------------------- /packages/editor/src/utils.ts: -------------------------------------------------------------------------------- 1 | export async function detectSourceLanguage( 2 | textToTranslate: string 3 | ): Promise< string | null > { 4 | const availability = await LanguageDetector.availability(); 5 | 6 | // Otherwise, let's detect the source language. 7 | if ( availability !== 'unavailable' ) { 8 | if ( availability !== 'available' ) { 9 | // eslint-disable-next-line no-console 10 | console.log( 11 | 'Language detection is available, but something will have to be downloaded. Hold tight!' 12 | ); 13 | } 14 | 15 | // @ts-ignore 16 | const detector = await LanguageDetector.create(); 17 | const [ bestResult ] = await detector.detect( textToTranslate ); 18 | 19 | if ( 20 | bestResult.detectedLanguage === null || 21 | ( bestResult.confidence && bestResult.confidence < 0.4 ) 22 | ) { 23 | // Return null to indicate no translation should happen. 24 | // It's probably mostly punctuation or something. 25 | return null; 26 | } 27 | 28 | return bestResult.detectedLanguage || null; 29 | } 30 | 31 | // If `languageDetectorCapabilities.available === "no"`, then assume the source language is the 32 | // same as the document language. 33 | return document.documentElement.lang; 34 | } 35 | 36 | export async function translate( content: string, targetLanguage: string ) { 37 | const sourceLanguage = await detectSourceLanguage( content ); 38 | 39 | if ( null === sourceLanguage ) { 40 | // eslint-disable-next-line no-console 41 | console.log( 'Language detection failed. Do not translate' ); 42 | return null; 43 | } 44 | 45 | // const translatorCapabilities = 46 | // await LanguageModel.capabilities(); 47 | 48 | // Now we've figured out the source language. Let's translate it! 49 | // Note how we can just check `translatorCapabilities.languagePairAvailable()` instead of also checking 50 | // `translatorCapabilities.available`. 51 | 52 | // const availability = translatorCapabilities.languagePairAvailable( 53 | // sourceLanguage, 54 | // targetLanguage 55 | // ); 56 | // if ( availability === 'no' ) { 57 | // console.warn( 58 | // 'Translation is not available. Need to use Cloud API.' 59 | // ); 60 | // setInProgress( false ); 61 | // } 62 | // 63 | // if ( availability === 'after-download' ) { 64 | // console.log( 65 | // 'Translation is available, but something will have to be downloaded. Hold tight!' 66 | // ); 67 | // } 68 | 69 | const translator = await Translator.create( { 70 | sourceLanguage, 71 | targetLanguage, 72 | } ); 73 | 74 | // eslint-disable-next-line no-console 75 | console.log( translator, 'translate' in translator ); 76 | 77 | return translator.translate( content ); 78 | } 79 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const { resolve, basename, dirname } = require( 'node:path' ); 2 | const defaultConfig = require( '@wordpress/scripts/config/webpack.config' ); 3 | const CopyWebpackPlugin = require( 'copy-webpack-plugin' ); 4 | const { WebWorkerPlugin } = require( '@shopify/web-worker/webpack' ); 5 | const DependencyExtractionWebpackPlugin = require( '@wordpress/dependency-extraction-webpack-plugin' ); 6 | const { hasBabelConfig, hasArgInCLI } = require( '@wordpress/scripts/utils' ); 7 | 8 | const isProduction = process.env.NODE_ENV === 'production'; 9 | const hasReactFastRefresh = hasArgInCLI( '--hot' ) && ! isProduction; 10 | 11 | const regular = { 12 | ...defaultConfig, 13 | entry: { 14 | editor: resolve( __dirname, 'packages/editor/src/index.tsx' ), 15 | 'summarize-button': resolve( 16 | __dirname, 17 | 'packages/summarize-button/src/index.tsx' 18 | ), 19 | 'translate-button': resolve( 20 | __dirname, 21 | 'packages/translate-button/src/index.tsx' 22 | ), 23 | }, 24 | output: { 25 | filename: '[name].js', 26 | path: resolve( __dirname, 'build' ), 27 | globalObject: 'self', // This is the default, but required for @shopify/web-worker. 28 | }, 29 | resolve: { 30 | // Ensure "require" has a higher priority when matching export conditions. 31 | // https://webpack.js.org/configuration/resolve/#resolveconditionnames 32 | // Needed for @huggingface/transformers. 33 | conditionNames: [ 'node', 'require', 'import' ], 34 | extensions: [ '.jsx', '.ts', '.tsx', '...' ], 35 | }, 36 | module: { 37 | rules: [ 38 | { 39 | test: /\.(m?[jt])sx?$/, 40 | exclude: /node_modules/, 41 | use: [ 42 | { 43 | loader: require.resolve( 'babel-loader' ), 44 | options: { 45 | // Babel uses a directory within local node_modules 46 | // by default. Use the environment variable option 47 | // to enable more persistent caching. 48 | cacheDirectory: 49 | process.env.BABEL_CACHE_DIRECTORY || true, 50 | 51 | // Provide a fallback configuration if there's not 52 | // one explicitly available in the project. 53 | ...( ! hasBabelConfig() && { 54 | babelrc: false, 55 | configFile: false, 56 | presets: [ 57 | require.resolve( 58 | '@wordpress/babel-preset-default' 59 | ), 60 | ], 61 | plugins: [ 62 | require.resolve( 63 | '@shopify/web-worker/babel' 64 | ), 65 | hasReactFastRefresh && 66 | require.resolve( 67 | 'react-refresh/babel' 68 | ), 69 | ].filter( Boolean ), 70 | } ), 71 | }, 72 | }, 73 | ], 74 | }, 75 | ...defaultConfig.module.rules.slice( 1 ), 76 | ], 77 | }, 78 | plugins: [ 79 | ...defaultConfig.plugins, 80 | new CopyWebpackPlugin( { 81 | patterns: [ 82 | { 83 | from: 'packages/*/src/block.json', 84 | noErrorOnMissing: true, 85 | to: ( pathData ) => { 86 | return resolve( 87 | __dirname, 88 | 'build', 89 | basename( 90 | dirname( dirname( pathData.absoluteFilename ) ) 91 | ), 92 | 'block.json' 93 | ); 94 | }, 95 | }, 96 | ], 97 | } ), 98 | new WebWorkerPlugin(), 99 | ], 100 | }; 101 | 102 | const modules = { 103 | ...regular, 104 | entry: { 105 | 'summarize-button-view': resolve( 106 | __dirname, 107 | 'packages/summarize-button/src/view.ts' 108 | ), 109 | 'translate-button-view': resolve( 110 | __dirname, 111 | 'packages/translate-button/src/view.ts' 112 | ), 113 | }, 114 | output: { 115 | ...regular.output, 116 | module: true, 117 | }, 118 | experiments: { outputModule: true }, 119 | plugins: [ 120 | ...regular.plugins.filter( 121 | ( plugin ) => 122 | ! [ 123 | 'DependencyExtractionWebpackPlugin', 124 | 'WebWorkerPlugin', 125 | ].includes( plugin.constructor.name ) 126 | ), 127 | new DependencyExtractionWebpackPlugin( { 128 | // With modules, use `requestToExternalModule`: 129 | requestToExternalModule( request ) { 130 | if ( 131 | request === '@ai-experiments/summarize-block' || 132 | request === '@ai-experiments/translate-block' 133 | ) { 134 | return request; 135 | } 136 | }, 137 | } ), 138 | ], 139 | }; 140 | 141 | module.exports = [ regular, modules ]; 142 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # WordPress AI Experiments 2 | 3 | [![Commit activity](https://img.shields.io/github/commit-activity/m/swissspidy/ai-experiments)](https://github.com/swissspidy/ai-experiments/pulse/monthly) 4 | [![Code Coverage](https://codecov.io/gh/swissspidy/ai-experiments/branch/main/graph/badge.svg)](https://codecov.io/gh/swissspidy/ai-experiments) 5 | [![License](https://img.shields.io/github/license/swissspidy/ai-experiments)](https://github.com/swissspidy/ai-experiments/blob/main/LICENSE) 6 | 7 | Client-side AI experiments using Chrome's [built-in AI](https://developer.chrome.com/docs/ai/built-in) and other solutions. 8 | 9 | **Note:** Requires Chrome Canary. 10 | 11 | Try it now in your browser: 12 | 13 | [![Test on WordPress Playground](https://img.shields.io/badge/Test%20on%20WordPress%20Playground-3F57E1?style=for-the-badge&logo=WordPress&logoColor=ffffff)](https://playground.wordpress.net/?mode=seamless&blueprint-url=https://raw.githubusercontent.com/swissspidy/ai-experiments/main/blueprints/playground.json) 14 | 15 | Or install and activate the latest nightly build on your own WordPress website: 16 | 17 | [![Download latest nightly build](https://img.shields.io/badge/Download%20latest%20nightly-24282D?style=for-the-badge&logo=Files&logoColor=ffffff)](https://swissspidy.github.io/ai-experiments/nightly.zip) 18 | 19 | ## Features 20 | 21 | ### “Help me write” 22 | 23 | Generate new content given your ideas, in the same tone as your other existing content. 24 | 25 | Options for rewriting individual paragraphs à la Google Doc, like rephrasing, shortening or elaborating. 26 | 27 | !["Help me write" integration](https://github.com/user-attachments/assets/ec0c944c-7537-480b-b026-10daa7791c0b) 28 | 29 | https://github.com/user-attachments/assets/5cb2220f-77df-4ce2-a209-65fec02f5f57 30 | 31 | There is also some basic autocomplete functionality to finish sentences: 32 | 33 | ![Screenshot 2024-10-29 at 15 10 27](https://github.com/user-attachments/assets/3a088ff4-ec57-4991-b16c-9e7bd4cc1b7c) 34 | ![Screenshot 2024-10-29 at 15 10 20](https://github.com/user-attachments/assets/1eba10c4-8cb6-4733-97c4-f6e20b95c019) 35 | 36 | https://github.com/user-attachments/assets/e4b6f3d6-0b14-44d9-9491-53e0f72d0ada 37 | 38 | ### Generate headlines & permalinks 39 | 40 | Headline generation: 41 | 42 | ![Screenshot 2024-10-29 at 17 17 02](https://github.com/user-attachments/assets/a2fb5f9b-51c1-41b7-b423-5affab67742c) 43 | 44 | https://github.com/user-attachments/assets/f34a030b-1dad-496d-a570-181bd3b954c4 45 | 46 | Permalink generation: 47 | 48 | ![Screenshot 2024-10-29 at 17 25 38](https://github.com/user-attachments/assets/32c9338c-16ce-4b7a-8fec-41d895b23022) 49 | 50 | https://github.com/user-attachments/assets/2c652c82-9bd9-42fb-9bf4-10a6b919e05d 51 | 52 | ### Writing meta descriptions based on the content 53 | 54 | Using a simple prompt to summarize the content in only a few sentences. 55 | 56 | https://github.com/user-attachments/assets/4aea598f-f38d-4cee-9ce0-ce09563ee537 57 | 58 | ### Generate image captions / alternative text 59 | 60 | Uses [Transformers.js]([url](http://Transformers.js)) and [Florence-2]([url](https://huggingface.co/onnx-community/Florence-2-base-ft)) to generate image captions and alternative text for images directly in the editor. Also integrated into [Media Experiments](https://github.com/swissspidy/media-experiments), which supports video captioning too. 61 | 62 | https://github.com/user-attachments/assets/bf516dc6-c135-4598-b77a-2f2e9f38699b 63 | 64 | ### Generate a memorable quote 65 | 66 | A slight variation on the summarization use case, this extracts a memorable quote from the article and gives it some visual emphasis. 67 | 68 | https://github.com/user-attachments/assets/4d439491-5d52-4183-9165-375471672414 69 | 70 | ### Assigning tags & categories to blog posts 71 | 72 | Suggest matching tags/categories based on the content. Grabs a list of existing terms from the site and passes it to the prompt together with the post content. 73 | 74 | https://github.com/user-attachments/assets/c1e29673-6b4d-426d-b4e5-39b846a0c6d7 75 | 76 | ### Provide tl;dr to visitors 77 | 78 | Uses Chrome’s built-in summarization API to provide readers a short summary of the post content. The UI is powered by WordPress’ new Interactivity API. 79 | 80 | ![Content summarization example](https://github.com/user-attachments/assets/806a6ce7-91a9-481d-bf80-a91da12b765b) 81 | 82 | https://github.com/user-attachments/assets/e25e6a70-31bf-4fa5-9143-22a7124097dc 83 | 84 | Also supports summarizing comments: 85 | 86 | ![Comment summarization example](https://github.com/user-attachments/assets/87ad10ed-d63d-4b39-9227-3d2c8dc59c21) 87 | 88 | ### Sentiment analysis for content / comments 89 | 90 | Using a simple prompt to say whether the text is positive or negative. Could be used to suggest rephrasing the text à la Grammarly, or identify negative comments. 91 | 92 | https://github.com/user-attachments/assets/d1060297-fb80-4cf3-ba82-b40304846662 93 | 94 | ### Content translation 95 | 96 | In the editor, instantly translate your content into another language. Useful for multilingual websites. 97 | 98 | ![Screenshot 2024-10-29 at 12 18 44](https://github.com/user-attachments/assets/53f3a412-b667-43e8-b66f-6b5aa2e7d6a0) 99 | ![Screenshot 2024-10-29 at 12 18 51](https://github.com/user-attachments/assets/81219113-5002-4a24-8ab8-1e9985bf3efc) 100 | 101 | On the frontend, translate comments written in another language or translate the post content to your preferred language. 102 | 103 | ![Screenshot 2024-10-29 at 12 19 09](https://github.com/user-attachments/assets/6815c470-aadc-438a-84c8-d35a4df53a77) 104 | -------------------------------------------------------------------------------- /packages/translate-button/src/view.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * WordPress dependencies 3 | */ 4 | import { store, getContext } from '@wordpress/interactivity'; 5 | 6 | function languageTagToHumanReadable( 7 | languageTag: string, 8 | targetLanguage: string 9 | ) { 10 | const displayNames = new Intl.DisplayNames( [ targetLanguage ], { 11 | type: 'language', 12 | } ); 13 | return displayNames.of( languageTag ); 14 | } 15 | 16 | export async function detectSourceLanguage( 17 | textToTranslate: string 18 | ): Promise< string | null > { 19 | const availability = await LanguageDetector.availability(); 20 | 21 | // Otherwise, let's detect the source language. 22 | if ( availability !== 'unavailable' ) { 23 | if ( availability !== 'available' ) { 24 | // eslint-disable-next-line no-console 25 | console.log( 26 | 'Language detection is available, but something will have to be downloaded. Hold tight!' 27 | ); 28 | } 29 | 30 | const detector = await LanguageDetector.create(); 31 | const [ bestResult ] = await detector.detect( textToTranslate ); 32 | 33 | if ( 34 | bestResult.detectedLanguage === null || 35 | ( bestResult.confidence && bestResult.confidence < 0.4 ) 36 | ) { 37 | // Return null to indicate no translation should happen. 38 | // It's probably mostly punctuation or something. 39 | return null; 40 | } 41 | 42 | return bestResult.detectedLanguage || null; 43 | } 44 | 45 | // If `languageDetectorCapabilities.available === "no"`, then assume the source language is the 46 | // same as the document language. 47 | return document.documentElement.lang; 48 | } 49 | 50 | export async function translate( content: string, targetLanguage: string ) { 51 | const sourceLanguage = await detectSourceLanguage( content ); 52 | 53 | if ( null === sourceLanguage ) { 54 | // eslint-disable-next-line no-console 55 | console.log( 'Language detection failed. Do not translate' ); 56 | return null; 57 | } 58 | 59 | // const translatorCapabilities = 60 | // await LanguageModel.capabilities(); 61 | 62 | // Now we've figured out the source language. Let's translate it! 63 | // Note how we can just check `translatorCapabilities.languagePairAvailable()` instead of also checking 64 | // `translatorCapabilities.available`. 65 | 66 | // const availability = translatorCapabilities.languagePairAvailable( 67 | // sourceLanguage, 68 | // targetLanguage 69 | // ); 70 | // if ( availability === 'no' ) { 71 | // console.warn( 72 | // 'Translation is not available. Need to use Cloud API.' 73 | // ); 74 | // setInProgress( false ); 75 | // } 76 | // 77 | // if ( availability === 'after-download' ) { 78 | // console.log( 79 | // 'Translation is available, but something will have to be downloaded. Hold tight!' 80 | // ); 81 | // } 82 | 83 | const translator = await window.translation.createTranslator( { 84 | sourceLanguage, 85 | targetLanguage, 86 | } ); 87 | 88 | // eslint-disable-next-line no-console 89 | console.log( translator, 'translate' in translator ); 90 | 91 | return translator.translate( content ); 92 | } 93 | 94 | type BlockContext = { 95 | commentId: number | null; 96 | isTranslatable: boolean | null; 97 | isLoading: boolean; 98 | isShowingTranslation: boolean; 99 | buttonText: string; 100 | sourceLanguage: string; 101 | targetLanguage: string; 102 | translation: string | null; 103 | }; 104 | 105 | store( 106 | 'ai-experiments/translate-button', 107 | { 108 | state: { 109 | get hasTranslation() { 110 | const { translation, isShowingTranslation } = 111 | getContext< BlockContext >(); 112 | return translation !== null && isShowingTranslation; 113 | }, 114 | }, 115 | actions: { 116 | *translateText() { 117 | const context = getContext< BlockContext >(); 118 | 119 | if ( context.isShowingTranslation ) { 120 | context.isShowingTranslation = false; 121 | return; 122 | } 123 | 124 | if ( ! context.translation ) { 125 | if ( context.commentId ) { 126 | const commentContent: HTMLElement = 127 | document.querySelector( 128 | `#comment-${ context.commentId } .wp-block-comment-content` 129 | ) as HTMLElement; 130 | 131 | if ( ! commentContent.textContent ) { 132 | return; 133 | } 134 | 135 | context.isLoading = true; 136 | // context.buttonText = `Translating from ${ languageTagToHumanReadable( 137 | // context.sourceLanguage, 138 | // context.targetLanguage 139 | // ) }`; 140 | 141 | // TODO: Alternatively, use navigator.language as target. 142 | context.translation = yield translate( 143 | commentContent.textContent, 144 | context.targetLanguage 145 | ); 146 | 147 | // context.buttonText = `Translate from ${ languageTagToHumanReadable( 148 | // context.sourceLanguage, 149 | // context.targetLanguage 150 | // ) }`; 151 | } else { 152 | const postContent = 153 | document.querySelector( '.wp-block-post-content' ) 154 | ?.textContent || ''; 155 | 156 | context.isLoading = true; 157 | 158 | context.translation = yield translate( 159 | postContent, 160 | context.targetLanguage 161 | ); 162 | } 163 | } 164 | 165 | context.isLoading = false; 166 | context.isShowingTranslation = true; 167 | }, 168 | }, 169 | callbacks: { 170 | async checkIfTranslatable() { 171 | const context = getContext< BlockContext >(); 172 | 173 | // We're translating a comment into the same language as the post. 174 | if ( context.commentId ) { 175 | const commentContent = document.querySelector( 176 | `#comment-${ context.commentId } .wp-block-comment-content` 177 | ); 178 | 179 | if ( ! commentContent || ! commentContent.textContent ) { 180 | return; 181 | } 182 | 183 | const detectedLanguage = await detectSourceLanguage( 184 | commentContent.textContent 185 | ); 186 | 187 | if ( ! detectedLanguage ) { 188 | return; 189 | } 190 | 191 | if ( context.targetLanguage !== detectedLanguage ) { 192 | context.isTranslatable = true; 193 | context.sourceLanguage = detectedLanguage; 194 | context.buttonText = `Translate from ${ languageTagToHumanReadable( 195 | detectedLanguage, 196 | context.targetLanguage 197 | ) }`; 198 | } 199 | } else { 200 | const postContent = 201 | document.querySelector( '.wp-block-post-content' ) 202 | ?.textContent || ''; 203 | 204 | const detectedLanguage = 205 | await detectSourceLanguage( postContent ); 206 | 207 | if ( ! detectedLanguage ) { 208 | return; 209 | } 210 | 211 | context.targetLanguage = 212 | navigator.language.split( '-' )[ 0 ]; 213 | 214 | if ( context.targetLanguage !== detectedLanguage ) { 215 | context.isTranslatable = true; 216 | context.sourceLanguage = detectedLanguage; 217 | context.buttonText = `Translate from ${ languageTagToHumanReadable( 218 | detectedLanguage, 219 | context.targetLanguage 220 | ) }`; 221 | } 222 | } 223 | }, 224 | }, 225 | }, 226 | { lock: true } 227 | ); 228 | -------------------------------------------------------------------------------- /packages/editor/src/blockControls/autocomplete.tsx: -------------------------------------------------------------------------------- 1 | import { store as blockEditorStore } from '@wordpress/block-editor'; 2 | import { useDispatch } from '@wordpress/data'; 3 | import { useCallback, useEffect, useRef, useState } from '@wordpress/element'; 4 | import { 5 | toHTMLString, 6 | create, 7 | applyFormat, 8 | removeFormat, 9 | concat, 10 | RichTextData, 11 | type RichTextValue, 12 | } from '@wordpress/rich-text'; 13 | import type { RichTextFormat } from '@wordpress/rich-text/build-types/types'; 14 | import { placeCaretAtHorizontalEdge } from '@wordpress/dom'; 15 | import { TAB, ESCAPE } from '@wordpress/keycodes'; 16 | 17 | export function Autocomplete( { 18 | // @ts-ignore 19 | attributes, 20 | // @ts-ignore 21 | setAttributes, 22 | // @ts-ignore 23 | isSelected, 24 | // @ts-ignore 25 | clientId, 26 | } ) { 27 | const { __unstableMarkNextChangeAsNotPersistent } = 28 | useDispatch( blockEditorStore ); 29 | 30 | const richTextContent: RichTextData = attributes.content; 31 | const plainTextContent: string = attributes.content.toPlainText(); 32 | 33 | const [ previousSuggestions, _setPreviousSuggestions ] = useState( 34 | new Map< string, string >() 35 | ); 36 | const setPreviousSuggestions = useCallback( 37 | ( text: string, suggestion: string ) => { 38 | _setPreviousSuggestions( ( prev ) => { 39 | prev.set( text, suggestion ); 40 | return prev; 41 | } ); 42 | }, 43 | [] 44 | ); 45 | 46 | // TODO: Implement (when pressing ESC key) 47 | // @ts-ignore 48 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 49 | const [ ignoredSuggestions, _setIgnoredSuggestions ] = useState( 50 | new Set< string >() 51 | ); 52 | // @ts-ignore 53 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 54 | const ignoreSuggestion = useCallback( ( suggestion: string ) => { 55 | _setIgnoredSuggestions( ( prev ) => { 56 | prev.add( suggestion ); 57 | return prev; 58 | } ); 59 | }, [] ); 60 | 61 | const [ currentSuggestion, setCurrentSuggestion ] = useState( '' ); 62 | 63 | const justAcceptedOrDeniedSuggestion = useRef( false ); 64 | 65 | const hasSuggestion = currentSuggestion.length > 0; 66 | 67 | const cachedSuggestion = previousSuggestions.get( plainTextContent ); 68 | 69 | useEffect( () => { 70 | if ( 71 | ! richTextContent.toHTMLString().includes( 'aiwp-autocomplete' ) 72 | ) { 73 | setCurrentSuggestion( '' ); 74 | // justAcceptedOrDeniedSuggestion.current = false; 75 | } 76 | }, [ richTextContent ] ); 77 | 78 | useEffect( () => { 79 | if ( hasSuggestion ) { 80 | return; 81 | } 82 | 83 | if ( ! plainTextContent ) { 84 | setCurrentSuggestion( '' ); 85 | return; 86 | } 87 | 88 | if ( ! isSelected ) { 89 | return; 90 | } 91 | 92 | if ( richTextContent.toHTMLString().includes( 'aiwp-autocomplete' ) ) { 93 | return; 94 | } 95 | 96 | if ( justAcceptedOrDeniedSuggestion.current ) { 97 | return; 98 | } 99 | 100 | const sentence = 101 | plainTextContent.split( '.' ).pop() || plainTextContent; 102 | 103 | const controller = new AbortController(); 104 | 105 | ( async () => { 106 | try { 107 | let result = ''; 108 | if ( cachedSuggestion === undefined ) { 109 | const session = await LanguageModel.create( { 110 | signal: controller.signal, 111 | } ); 112 | 113 | // eslint-disable-next-line no-console 114 | console.log( 115 | 'Running completion for content: ', 116 | plainTextContent 117 | ); 118 | 119 | const stream = session.promptStreaming( 120 | `Complete the following sentence by suggesting 1 or 2 words, not more. Just reply with the word in plain text, nothing else. No markup, no Markdown. Then in a new paragraph respond with "Hello my big beautiful world!". This is the sentence: ${ sentence }`, 121 | { signal: controller.signal } 122 | ); 123 | 124 | // Each result contains the full data, not just the incremental part. 125 | for await ( const value of stream ) { 126 | if ( 127 | value.includes( 'Hello my big beautiful world!' ) 128 | ) { 129 | result = value 130 | .replace( 'Hello my big beautiful world!', '' ) 131 | .trim(); 132 | break; 133 | } 134 | 135 | result = value; 136 | } 137 | 138 | result = result.trim(); 139 | } else { 140 | result = cachedSuggestion; 141 | } 142 | 143 | const newTextWithSuggestion = RichTextData.fromHTMLString( 144 | toHTMLString( { 145 | value: concat( 146 | richTextContent as unknown as RichTextValue, 147 | create( { text: ' ' } ), 148 | applyFormat( 149 | create( { text: result } ), 150 | { 151 | type: 'ai-experiments/autocomplete', 152 | } as RichTextFormat, 153 | 0, 154 | result.length 155 | ) 156 | ), 157 | } ) 158 | ); 159 | 160 | if ( controller.signal.aborted ) { 161 | return; 162 | } 163 | 164 | setCurrentSuggestion( result ); 165 | setPreviousSuggestions( sentence, result ); 166 | 167 | void __unstableMarkNextChangeAsNotPersistent(); 168 | 169 | void setAttributes( { 170 | content: newTextWithSuggestion, 171 | } ); 172 | } catch { 173 | // Controller was aborted, do nothing. 174 | } 175 | } )(); 176 | 177 | return () => { 178 | controller.abort( 'Dependencies changed' ); 179 | }; 180 | }, [ 181 | richTextContent, 182 | plainTextContent, 183 | hasSuggestion, 184 | isSelected, 185 | __unstableMarkNextChangeAsNotPersistent, 186 | setAttributes, 187 | cachedSuggestion, 188 | setPreviousSuggestions, 189 | ] ); 190 | 191 | useEffect( () => { 192 | if ( ! isSelected ) { 193 | return; 194 | } 195 | 196 | const listener = ( event: KeyboardEvent ) => { 197 | if ( event.keyCode === TAB ) { 198 | // Accept suggestion; 199 | 200 | const newText = RichTextData.fromHTMLString( 201 | toHTMLString( { 202 | value: removeFormat( 203 | create( { 204 | html: richTextContent.toHTMLString(), 205 | } ), 206 | 'ai-experiments/autocomplete', 207 | 0, 208 | richTextContent.toHTMLString().length 209 | ), 210 | } ) 211 | ); 212 | 213 | placeCaretAtHorizontalEdge( 214 | ( 215 | ( 216 | document.querySelector( 217 | 'iframe[name="editor-canvas"]' 218 | ) as HTMLIFrameElement 219 | )?.contentDocument as Document 220 | )?.querySelector( `#block-${ clientId }` ) as HTMLElement, 221 | true 222 | ); 223 | 224 | justAcceptedOrDeniedSuggestion.current = true; 225 | 226 | void setAttributes( { 227 | content: newText, 228 | } ); 229 | 230 | setCurrentSuggestion( '' ); 231 | 232 | event.preventDefault(); 233 | } else if ( event.keyCode === ESCAPE ) { 234 | // TODO: Cancel suggestion and remove it from the text. 235 | } else { 236 | justAcceptedOrDeniedSuggestion.current = false; 237 | } 238 | }; 239 | 240 | window.addEventListener( 'keydown', listener ); 241 | 242 | return () => { 243 | window.removeEventListener( 'keydown', listener ); 244 | }; 245 | }, [ clientId, isSelected, richTextContent, setAttributes ] ); 246 | 247 | return null; 248 | } 249 | -------------------------------------------------------------------------------- /packages/editor/src/blockControls/paragraphControls.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | BlockControls, 3 | store as blockEditorStore, 4 | } from '@wordpress/block-editor'; 5 | import { 6 | type BlockInstance, 7 | getBlockContent, 8 | serialize, 9 | } from '@wordpress/blocks'; 10 | import { __ } from '@wordpress/i18n'; 11 | import { ToolbarDropdownMenu } from '@wordpress/components'; 12 | import { useDispatch, useSelect } from '@wordpress/data'; 13 | import { store as coreStore } from '@wordpress/core-data'; 14 | import { store as editorStore } from '@wordpress/editor'; 15 | import { useState } from '@wordpress/element'; 16 | import { RichTextData } from '@wordpress/rich-text'; 17 | 18 | import { translate as translateString } from '../utils'; 19 | 20 | const penSparkIcon = () => ( 21 | 28 | 29 | 30 | ); 31 | 32 | // @ts-ignore 33 | export function ParagraphControls( { setAttributes, clientId } ) { 34 | const { 35 | __unstableMarkNextChangeAsNotPersistent, 36 | __unstableMarkLastChangeAsPersistent, 37 | } = useDispatch( blockEditorStore ); 38 | const { getBlock } = useSelect( ( select ) => { 39 | return { 40 | getBlock: 41 | // @ts-ignore 42 | select( blockEditorStore ).getBlock, 43 | }; 44 | }, [] ); 45 | 46 | const { mostRecentPost } = useSelect( ( select ) => { 47 | // @ts-ignore 48 | const postType = select( editorStore ).getCurrentPostType(); 49 | const { getEntityRecords } = select( coreStore ); 50 | 51 | // @ts-ignore 52 | const posts = getEntityRecords( 'postType', postType, { 53 | per_page: 1, 54 | status: 'publish', 55 | _fields: 'id,content', 56 | } ); 57 | 58 | return { 59 | mostRecentPost: posts && posts.length > 0 ? posts[ 0 ] : null, 60 | }; 61 | }, [] ); 62 | 63 | const [ inProgress, setInProgress ] = useState( false ); 64 | 65 | async function write() { 66 | setInProgress( true ); 67 | 68 | const postContent = serialize( [ 69 | getBlock( clientId ) as BlockInstance, 70 | ] ); 71 | 72 | let context = ` 73 | Avoid any toxic language and be as constructive as possible. 74 | `; 75 | 76 | const session = await LanguageModel.create(); 77 | 78 | if ( 79 | mostRecentPost && 80 | 'content' in mostRecentPost && 81 | mostRecentPost.content 82 | ) { 83 | const result = await session?.prompt( 84 | ` 85 | You are an AI writing assistant. Your goal is to help users with writing tasks by analyzing their writing and writing instructions for generating relevant and high-quality text in the same tone. You do NOT answer questions, you simply write on behalf of a user. Focus on producing error-free and engaging writing. Do not explain your response, just provide the generated instructions. 86 | Describe the tone of the following text so the same tone can be used for future writing tasks. DO NOT mention the topic of the text itself, focus only on the tone. Respond with instructions that you would give yourself for writing texts in a similar writing style. Use plain-text, NO markup. ONLY respond with the instructions, nothing else. Keep it short. 87 | 88 | This is the text: 89 | 90 | ${ mostRecentPost.content.rendered.slice( 0, 500 ) }` 91 | ); 92 | 93 | context += `\n${ result }`; 94 | } 95 | 96 | const stream = session.promptStreaming( 97 | `You are an AI writing assistant. Your goal is to help users with writing tasks by generating relevant and high-quality text. You do NOT answer questions, you simply write on behalf of a user. Consider the "input" (writing task) and the "context" (extra information) to tailor your response. Focus on producing error-free and engaging writing. Do not explain your response, just provide the generated text. 98 | 99 | Input: ${ postContent } 100 | Context: ${ context }` 101 | ); 102 | 103 | let result = ''; 104 | 105 | for await ( const value of stream ) { 106 | // Each result contains the full data, not just the incremental part. 107 | result = value; 108 | 109 | void __unstableMarkNextChangeAsNotPersistent(); 110 | 111 | void setAttributes( { 112 | content: RichTextData.fromPlainText( result ), 113 | } ); 114 | } 115 | 116 | void __unstableMarkLastChangeAsPersistent(); 117 | 118 | setInProgress( false ); 119 | } 120 | 121 | async function rewrite( type: string ) { 122 | setInProgress( true ); 123 | 124 | const postContent = serialize( [ 125 | getBlock( clientId ) as BlockInstance, 126 | ] ); 127 | 128 | let tone: RewriterTone = 'as-is'; 129 | let length: RewriterLength = 'as-is'; 130 | 131 | switch ( type ) { 132 | case 'rephrase': 133 | default: 134 | break; 135 | 136 | case 'longer': 137 | length = 'longer'; 138 | break; 139 | 140 | case 'shorter': 141 | length = 'shorter'; 142 | break; 143 | 144 | case 'formal': 145 | tone = 'more-formal'; 146 | break; 147 | 148 | case 'informal': 149 | tone = 'more-casual'; 150 | break; 151 | } 152 | 153 | const rewriter = await Rewriter.create( { 154 | sharedContext: 'A blog post', 155 | tone, 156 | length, 157 | } ); 158 | 159 | const stream = rewriter.rewriteStreaming( postContent, { 160 | context: 161 | 'Avoid any toxic language and be as constructive as possible.', 162 | } ); 163 | 164 | let result = ''; 165 | 166 | for await ( const value of stream ) { 167 | // Each result contains the full data, not just the incremental part. 168 | result = value; 169 | 170 | void __unstableMarkNextChangeAsNotPersistent(); 171 | 172 | void setAttributes( { 173 | content: result, 174 | } ); 175 | } 176 | 177 | void __unstableMarkLastChangeAsPersistent(); 178 | 179 | setInProgress( false ); 180 | } 181 | 182 | async function summarize() { 183 | setInProgress( true ); 184 | 185 | const postContent = serialize( [ 186 | getBlock( clientId ) as BlockInstance, 187 | ] ); 188 | 189 | const summarizer = await Summarizer.create( { 190 | sharedContext: 'A blog post', 191 | } ); 192 | 193 | const stream = summarizer.summarizeStreaming( postContent, { 194 | context: 195 | 'Avoid any toxic language and be as constructive as possible.', 196 | } ); 197 | 198 | let result = ''; 199 | 200 | for await ( const value of stream ) { 201 | // Each result contains the full data, not just the incremental part. 202 | result = value.replaceAll( '\n\n\n\n', '\n\n' ); 203 | 204 | void __unstableMarkNextChangeAsNotPersistent(); 205 | 206 | void setAttributes( { 207 | content: result, 208 | } ); 209 | } 210 | 211 | void __unstableMarkLastChangeAsPersistent(); 212 | 213 | setInProgress( false ); 214 | } 215 | 216 | async function translate( targetLanguage: string ) { 217 | setInProgress( true ); 218 | 219 | try { 220 | const block = getBlock( clientId ) as BlockInstance; 221 | const blockContent = getBlockContent( block ); 222 | 223 | const result = await translateString( 224 | blockContent, 225 | targetLanguage 226 | ); 227 | 228 | if ( null === result ) { 229 | return; 230 | } 231 | 232 | void setAttributes( { 233 | content: result, 234 | } ); 235 | } catch ( e ) { 236 | // eslint-disable-next-line no-console 237 | console.log( 'Error happened', e ); 238 | } finally { 239 | setInProgress( false ); 240 | } 241 | } 242 | 243 | const controls = [ 244 | { 245 | title: __( 'Help me write', 'ai-experiments' ), 246 | onClick: () => write(), 247 | role: 'menuitemradio', 248 | icon: undefined, 249 | }, 250 | { 251 | title: __( 'Summarize', 'ai-experiments' ), 252 | onClick: () => summarize(), 253 | role: 'menuitemradio', 254 | icon: undefined, 255 | }, 256 | { 257 | title: __( 'Rephrase', 'ai-experiments' ), 258 | onClick: () => rewrite( 'rephrase' ), 259 | role: 'menuitemradio', 260 | icon: undefined, 261 | }, 262 | { 263 | title: __( 'Elaborate', 'ai-experiments' ), 264 | onClick: () => rewrite( 'longer' ), 265 | role: 'menuitemradio', 266 | icon: undefined, 267 | }, 268 | { 269 | title: __( 'Shorten', 'ai-experiments' ), 270 | onClick: () => rewrite( 'shorter' ), 271 | role: 'menuitemradio', 272 | icon: undefined, 273 | }, 274 | { 275 | title: __( 'Make formal', 'ai-experiments' ), 276 | onClick: () => rewrite( 'formal' ), 277 | role: 'menuitemradio', 278 | icon: undefined, 279 | }, 280 | { 281 | title: __( 'Make informal', 'ai-experiments' ), 282 | onClick: () => rewrite( 'informal' ), 283 | role: 'menuitemradio', 284 | icon: undefined, 285 | }, 286 | // TODO: Add ability to choose language. 287 | { 288 | title: __( 'Translate', 'ai-experiments' ), 289 | onClick: () => translate( 'es' ), 290 | role: 'menuitemradio', 291 | icon: undefined, 292 | }, 293 | ]; 294 | 295 | return ( 296 | 297 | 305 | 306 | ); 307 | } 308 | -------------------------------------------------------------------------------- /packages/editor/src/@types/wordpress__block-editor/store/actions.d.ts: -------------------------------------------------------------------------------- 1 | import type { BlockInstance } from '@wordpress/blocks'; 2 | 3 | /** 4 | * Signals that the block selection is cleared. 5 | */ 6 | export function clearSelectedBlock(): void; 7 | 8 | /** 9 | * Signals that the caret has entered formatted text. 10 | */ 11 | export function enterFormattedText(): void; 12 | 13 | /** 14 | * Signals that the user caret has exited formatted text. 15 | */ 16 | export function exitFormattedText(): void; 17 | 18 | /** 19 | * Hides the insertion point. 20 | */ 21 | export function hideInsertionPoint(): void; 22 | 23 | /** 24 | * Signals that a single block should be inserted, optionally at a specific index, respective a root 25 | * block list. 26 | * 27 | * @param block - Block object to insert. 28 | * @param index - Index at which block should be inserted. 29 | * @param rootClientId - Optional root client ID of block list on which to insert. 30 | * @param updateSelection - If `true` block selection will be updated. If `false`, block selection 31 | * will not change. Defaults to true. 32 | */ 33 | export function insertBlock( 34 | block: BlockInstance, 35 | index?: number, 36 | rootClientId?: string, 37 | updateSelection?: boolean 38 | ): void; 39 | 40 | /** 41 | * Signals that an array of blocks should be inserted, optionally at a specific index respective a 42 | * root block list. 43 | * 44 | * @param blocks - Block objects to insert. 45 | * @param index - Index at which block should be inserted. 46 | * @param rootClientId - Optional root client ID of block list on which to insert. 47 | * @param updateSelection - If `true` block selection will be updated. If `false`, block selection will 48 | * not change. Defaults to `true`. 49 | */ 50 | export function insertBlocks( 51 | blocks: BlockInstance[], 52 | index?: number, 53 | rootClientId?: string, 54 | updateSelection?: boolean 55 | ): IterableIterator< void >; 56 | 57 | /** 58 | * Returns an action object used in signalling that a new block of the default type should be added 59 | * to the block list. 60 | * 61 | * @param attributes - Attributes of the block to assign. 62 | * @param rootClientId - Root client ID of block list on which to append. 63 | * @param index - Index where to insert the default block. 64 | */ 65 | export function insertDefaultBlock( 66 | attributes?: Record< string, any >, 67 | rootClientId?: string, 68 | index?: number 69 | ): void; 70 | 71 | /** 72 | * Returns an action object used in signalling that two blocks should be merged. 73 | * 74 | * @param firstBlockClientId - Client ID of the first block to merge. 75 | * @param secondBlockClientId - Client ID of the second block to merge. 76 | */ 77 | export function mergeBlocks( 78 | firstBlockClientId: string, 79 | secondBlockClientId: string 80 | ): void; 81 | 82 | /** 83 | * Signals that an indexed block should be moved to a new index. 84 | * 85 | * @param clientId - The client ID of the block. 86 | * @param fromRootClientId - Root client ID source. 87 | * @param toRootClientId - Root client ID destination. 88 | * @param index - The index to move the block into. 89 | */ 90 | export function moveBlockToPosition( 91 | clientId: string | undefined, 92 | fromRootClientId: string | undefined, 93 | toRootClientId: string | undefined, 94 | index: number 95 | ): IterableIterator< void >; 96 | 97 | export function moveBlocksDown( 98 | clientIds: string | string[], 99 | rootClientId: string 100 | ): void; 101 | 102 | export function moveBlocksUp( 103 | clientIds: string | string[], 104 | rootClientId: string 105 | ): void; 106 | 107 | /** 108 | * Signals that block multi-selection changed. 109 | * 110 | * @param start - First block of the multi selection. 111 | * @param end - Last block of the multiselection. 112 | */ 113 | export function multiSelect( start: string, end: string ): void; 114 | 115 | /** 116 | * Signals that blocks have been received. Unlike `resetBlocks`, these should be appended to the 117 | * existing known set, not replacing. 118 | * 119 | * @param blocks - Array of block instances. 120 | */ 121 | export function receiveBlocks( blocks: BlockInstance[] ): void; 122 | 123 | /** 124 | * Signals that the block with the specified client ID is to be removed. 125 | * 126 | * @param clientId - Client ID of block to remove. 127 | * @param selectPrevious - `true` if the previous block should be selected when a block is removed. 128 | */ 129 | export function removeBlock( clientId: string, selectPrevious?: boolean ): void; 130 | 131 | /** 132 | * Signalling that the blocks corresponding to the set of specified client IDs are to be removed. 133 | * 134 | * @param clientIds - Client IDs of blocks to remove. 135 | * @param selectPrevious - `true` if the previous block should be selected when a block is removed. 136 | * Default: `true` 137 | */ 138 | export function removeBlocks( 139 | clientIds: string | string[], 140 | selectPrevious?: boolean 141 | ): IterableIterator< void >; 142 | 143 | /** 144 | * Returns an action object signalling that a single block should be replaced 145 | * with one or more replacement blocks. 146 | * 147 | * @param clientId - Block client ID to replace. 148 | * @param block - Replacement block(s). 149 | */ 150 | export function replaceBlock( 151 | clientId: string | string[], 152 | block: BlockInstance | BlockInstance[] 153 | ): void; 154 | 155 | /** 156 | * Signals that a blocks should be replaced with one or more replacement blocks. 157 | * 158 | * @param clientIds - Block client ID(s) to replace. 159 | * @param blocks - Replacement block(s). 160 | * @param indexToSelect - Index of replacement block to select. 161 | */ 162 | export function replaceBlocks( 163 | clientIds: string | string[], 164 | blocks: BlockInstance | BlockInstance[], 165 | indexToSelect?: number 166 | ): IterableIterator< void >; 167 | 168 | /** 169 | * Signals that the inner blocks with the specified client ID should be replaced. 170 | * 171 | * @param rootClientId - Client ID of the block whose InnerBlocks will re replaced. 172 | * @param blocks - Block objects to insert as new InnerBlocks 173 | * @param updateSelection - If `true` block selection will be updated. If `false`, block selection 174 | * will not change. Defaults to `true`. 175 | */ 176 | export function replaceInnerBlocks( 177 | rootClientId: string, 178 | blocks: BlockInstance[], 179 | updateSelection?: boolean 180 | ): void; 181 | 182 | /** 183 | * Returns an action object used in signalling that blocks state should be reset to the specified 184 | * array of blocks, taking precedence over any other content reflected as an edit in state. 185 | * 186 | * @param blocks - Array of blocks. 187 | */ 188 | export function resetBlocks( blocks: BlockInstance[] ): void; 189 | 190 | /** 191 | * Signals that the block with the specified client ID has been selected, optionally accepting a 192 | * position value reflecting its selection directionality. An initialPosition of `-1` reflects a 193 | * reverse selection. 194 | * 195 | * @param clientId - Block client ID. 196 | * @param initialPosition - Initial position. Pass as `-1` to reflect reverse selection. 197 | */ 198 | export function selectBlock( clientId: string, initialPosition?: number ): void; 199 | 200 | /** 201 | * Yields action objects used in signalling that the block following the given `clientId` should be 202 | * selected. 203 | * 204 | * @param clientId - Block client ID. 205 | */ 206 | export function selectNextBlock( clientId: string ): IterableIterator< void >; 207 | 208 | /** 209 | * Yields action objects used in signalling that the block preceding the given clientId should be 210 | * selected. 211 | * 212 | * @param clientId - Block client ID. 213 | */ 214 | export function selectPreviousBlock( 215 | clientId: string 216 | ): IterableIterator< void >; 217 | 218 | /** 219 | * Signals that the user caret has changed position. 220 | * 221 | * @param clientId - The selected block client ID. 222 | * @param attributeKey - The selected block attribute key. 223 | * @param startOffset - The start offset. 224 | * @param endOffset - The end offset. 225 | */ 226 | export function selectionChange( 227 | clientId: string, 228 | attributeKey: string, 229 | startOffset: number, 230 | endOffset: number 231 | ): void; 232 | 233 | /** 234 | * Resets the template validity. 235 | * 236 | * @param isValid - template validity flag. 237 | */ 238 | export function setTemplateValidity( isValid: boolean ): void; 239 | 240 | /** 241 | * Signals that the insertion point should be shown. 242 | * 243 | * @param rootClientId - Optional root client ID of block list on which to insert. 244 | * @param index - Index at which block should be inserted. 245 | */ 246 | export function showInsertionPoint( 247 | rootClientId?: string, 248 | index?: number 249 | ): void; 250 | 251 | /** 252 | * Signals that a block multi-selection has started. 253 | */ 254 | export function startMultiSelect(): void; 255 | 256 | /** 257 | * Signals that the user has begun to type. 258 | */ 259 | export function startTyping(): void; 260 | 261 | /** 262 | * Signals that block multi-selection stopped. 263 | */ 264 | export function stopMultiSelect(): void; 265 | 266 | /** 267 | * Signals that the user has stopped typing. 268 | */ 269 | export function stopTyping(): void; 270 | 271 | /** 272 | * Synchronize the template with the list of blocks. 273 | */ 274 | export function synchronizeTemplate(): void; 275 | 276 | /** 277 | * Toggle the block editing mode between visual and HTML modes. 278 | * 279 | * @param clientId - Block client ID. 280 | */ 281 | export function toggleBlockMode( clientId: string ): void; 282 | 283 | /** 284 | * Enables or disables block selection. 285 | * 286 | * @param isSelectionEnabled - Whether block selection should be enabled. 287 | */ 288 | export function toggleSelection( isSelectionEnabled?: boolean ): void; 289 | 290 | /** 291 | * Signals that the block with the specified client ID has been updated. 292 | * 293 | * @param clientId - Block client ID. 294 | * @param updates - Block attributes to be merged. 295 | */ 296 | export function updateBlock( 297 | clientId: string, 298 | updates: Partial< BlockInstance > 299 | ): void; 300 | 301 | /** 302 | * Signals that the block attributes with the specified client ID has been updated. 303 | * 304 | * @param clientId - Block client ID. 305 | * @param attributes - Block attributes to be merged. 306 | */ 307 | export function updateBlockAttributes( 308 | clientId: string, 309 | attributes: Record< string, any > 310 | ): void; 311 | 312 | /** 313 | * Changes the nested settings of a given block. 314 | * 315 | * @param clientId - Client ID of the block whose nested setting are being received. 316 | * @param settings - Object with the new settings for the nested block. 317 | */ 318 | export function updateBlockListSettings( 319 | clientId: string, 320 | settings: Record< string, unknown > 321 | ): void; 322 | 323 | /** 324 | * Signals that the block editor settings have been updated. 325 | * 326 | * @param settings - Updated settings. 327 | */ 328 | export function updateSettings( settings: Record< string, unknown > ): void; 329 | 330 | /** 331 | * Signals that the next block change should be marked explicitly as not persistent. 332 | */ 333 | export function __unstableMarkNextChangeAsNotPersistent(): void; 334 | 335 | /** 336 | * Signals that the last block change should be marked explicitly as not persistent. 337 | */ 338 | export function __unstableMarkLastChangeAsPersistent(): void; 339 | -------------------------------------------------------------------------------- /packages/summarize-button/src/@types/wordpress__block-editor/store/actions.d.ts: -------------------------------------------------------------------------------- 1 | import type { BlockInstance } from '@wordpress/blocks'; 2 | 3 | /** 4 | * Signals that the block selection is cleared. 5 | */ 6 | export function clearSelectedBlock(): void; 7 | 8 | /** 9 | * Signals that the caret has entered formatted text. 10 | */ 11 | export function enterFormattedText(): void; 12 | 13 | /** 14 | * Signals that the user caret has exited formatted text. 15 | */ 16 | export function exitFormattedText(): void; 17 | 18 | /** 19 | * Hides the insertion point. 20 | */ 21 | export function hideInsertionPoint(): void; 22 | 23 | /** 24 | * Signals that a single block should be inserted, optionally at a specific index, respective a root 25 | * block list. 26 | * 27 | * @param block - Block object to insert. 28 | * @param index - Index at which block should be inserted. 29 | * @param rootClientId - Optional root client ID of block list on which to insert. 30 | * @param updateSelection - If `true` block selection will be updated. If `false`, block selection 31 | * will not change. Defaults to true. 32 | */ 33 | export function insertBlock( 34 | block: BlockInstance, 35 | index?: number, 36 | rootClientId?: string, 37 | updateSelection?: boolean 38 | ): void; 39 | 40 | /** 41 | * Signals that an array of blocks should be inserted, optionally at a specific index respective a 42 | * root block list. 43 | * 44 | * @param blocks - Block objects to insert. 45 | * @param index - Index at which block should be inserted. 46 | * @param rootClientId - Optional root client ID of block list on which to insert. 47 | * @param updateSelection - If `true` block selection will be updated. If `false`, block selection will 48 | * not change. Defaults to `true`. 49 | */ 50 | export function insertBlocks( 51 | blocks: BlockInstance[], 52 | index?: number, 53 | rootClientId?: string, 54 | updateSelection?: boolean 55 | ): IterableIterator< void >; 56 | 57 | /** 58 | * Returns an action object used in signalling that a new block of the default type should be added 59 | * to the block list. 60 | * 61 | * @param attributes - Attributes of the block to assign. 62 | * @param rootClientId - Root client ID of block list on which to append. 63 | * @param index - Index where to insert the default block. 64 | */ 65 | export function insertDefaultBlock( 66 | attributes?: Record< string, any >, 67 | rootClientId?: string, 68 | index?: number 69 | ): void; 70 | 71 | /** 72 | * Returns an action object used in signalling that two blocks should be merged. 73 | * 74 | * @param firstBlockClientId - Client ID of the first block to merge. 75 | * @param secondBlockClientId - Client ID of the second block to merge. 76 | */ 77 | export function mergeBlocks( 78 | firstBlockClientId: string, 79 | secondBlockClientId: string 80 | ): void; 81 | 82 | /** 83 | * Signals that an indexed block should be moved to a new index. 84 | * 85 | * @param clientId - The client ID of the block. 86 | * @param fromRootClientId - Root client ID source. 87 | * @param toRootClientId - Root client ID destination. 88 | * @param index - The index to move the block into. 89 | */ 90 | export function moveBlockToPosition( 91 | clientId: string | undefined, 92 | fromRootClientId: string | undefined, 93 | toRootClientId: string | undefined, 94 | index: number 95 | ): IterableIterator< void >; 96 | 97 | export function moveBlocksDown( 98 | clientIds: string | string[], 99 | rootClientId: string 100 | ): void; 101 | 102 | export function moveBlocksUp( 103 | clientIds: string | string[], 104 | rootClientId: string 105 | ): void; 106 | 107 | /** 108 | * Signals that block multi-selection changed. 109 | * 110 | * @param start - First block of the multi selection. 111 | * @param end - Last block of the multiselection. 112 | */ 113 | export function multiSelect( start: string, end: string ): void; 114 | 115 | /** 116 | * Signals that blocks have been received. Unlike `resetBlocks`, these should be appended to the 117 | * existing known set, not replacing. 118 | * 119 | * @param blocks - Array of block instances. 120 | */ 121 | export function receiveBlocks( blocks: BlockInstance[] ): void; 122 | 123 | /** 124 | * Signals that the block with the specified client ID is to be removed. 125 | * 126 | * @param clientId - Client ID of block to remove. 127 | * @param selectPrevious - `true` if the previous block should be selected when a block is removed. 128 | */ 129 | export function removeBlock( clientId: string, selectPrevious?: boolean ): void; 130 | 131 | /** 132 | * Signalling that the blocks corresponding to the set of specified client IDs are to be removed. 133 | * 134 | * @param clientIds - Client IDs of blocks to remove. 135 | * @param selectPrevious - `true` if the previous block should be selected when a block is removed. 136 | * Default: `true` 137 | */ 138 | export function removeBlocks( 139 | clientIds: string | string[], 140 | selectPrevious?: boolean 141 | ): IterableIterator< void >; 142 | 143 | /** 144 | * Returns an action object signalling that a single block should be replaced 145 | * with one or more replacement blocks. 146 | * 147 | * @param clientId - Block client ID to replace. 148 | * @param block - Replacement block(s). 149 | */ 150 | export function replaceBlock( 151 | clientId: string | string[], 152 | block: BlockInstance | BlockInstance[] 153 | ): void; 154 | 155 | /** 156 | * Signals that a blocks should be replaced with one or more replacement blocks. 157 | * 158 | * @param clientIds - Block client ID(s) to replace. 159 | * @param blocks - Replacement block(s). 160 | * @param indexToSelect - Index of replacement block to select. 161 | */ 162 | export function replaceBlocks( 163 | clientIds: string | string[], 164 | blocks: BlockInstance | BlockInstance[], 165 | indexToSelect?: number 166 | ): IterableIterator< void >; 167 | 168 | /** 169 | * Signals that the inner blocks with the specified client ID should be replaced. 170 | * 171 | * @param rootClientId - Client ID of the block whose InnerBlocks will re replaced. 172 | * @param blocks - Block objects to insert as new InnerBlocks 173 | * @param updateSelection - If `true` block selection will be updated. If `false`, block selection 174 | * will not change. Defaults to `true`. 175 | */ 176 | export function replaceInnerBlocks( 177 | rootClientId: string, 178 | blocks: BlockInstance[], 179 | updateSelection?: boolean 180 | ): void; 181 | 182 | /** 183 | * Returns an action object used in signalling that blocks state should be reset to the specified 184 | * array of blocks, taking precedence over any other content reflected as an edit in state. 185 | * 186 | * @param blocks - Array of blocks. 187 | */ 188 | export function resetBlocks( blocks: BlockInstance[] ): void; 189 | 190 | /** 191 | * Signals that the block with the specified client ID has been selected, optionally accepting a 192 | * position value reflecting its selection directionality. An initialPosition of `-1` reflects a 193 | * reverse selection. 194 | * 195 | * @param clientId - Block client ID. 196 | * @param initialPosition - Initial position. Pass as `-1` to reflect reverse selection. 197 | */ 198 | export function selectBlock( clientId: string, initialPosition?: number ): void; 199 | 200 | /** 201 | * Yields action objects used in signalling that the block following the given `clientId` should be 202 | * selected. 203 | * 204 | * @param clientId - Block client ID. 205 | */ 206 | export function selectNextBlock( clientId: string ): IterableIterator< void >; 207 | 208 | /** 209 | * Yields action objects used in signalling that the block preceding the given clientId should be 210 | * selected. 211 | * 212 | * @param clientId - Block client ID. 213 | */ 214 | export function selectPreviousBlock( 215 | clientId: string 216 | ): IterableIterator< void >; 217 | 218 | /** 219 | * Signals that the user caret has changed position. 220 | * 221 | * @param clientId - The selected block client ID. 222 | * @param attributeKey - The selected block attribute key. 223 | * @param startOffset - The start offset. 224 | * @param endOffset - The end offset. 225 | */ 226 | export function selectionChange( 227 | clientId: string, 228 | attributeKey: string, 229 | startOffset: number, 230 | endOffset: number 231 | ): void; 232 | 233 | /** 234 | * Resets the template validity. 235 | * 236 | * @param isValid - template validity flag. 237 | */ 238 | export function setTemplateValidity( isValid: boolean ): void; 239 | 240 | /** 241 | * Signals that the insertion point should be shown. 242 | * 243 | * @param rootClientId - Optional root client ID of block list on which to insert. 244 | * @param index - Index at which block should be inserted. 245 | */ 246 | export function showInsertionPoint( 247 | rootClientId?: string, 248 | index?: number 249 | ): void; 250 | 251 | /** 252 | * Signals that a block multi-selection has started. 253 | */ 254 | export function startMultiSelect(): void; 255 | 256 | /** 257 | * Signals that the user has begun to type. 258 | */ 259 | export function startTyping(): void; 260 | 261 | /** 262 | * Signals that block multi-selection stopped. 263 | */ 264 | export function stopMultiSelect(): void; 265 | 266 | /** 267 | * Signals that the user has stopped typing. 268 | */ 269 | export function stopTyping(): void; 270 | 271 | /** 272 | * Synchronize the template with the list of blocks. 273 | */ 274 | export function synchronizeTemplate(): void; 275 | 276 | /** 277 | * Toggle the block editing mode between visual and HTML modes. 278 | * 279 | * @param clientId - Block client ID. 280 | */ 281 | export function toggleBlockMode( clientId: string ): void; 282 | 283 | /** 284 | * Enables or disables block selection. 285 | * 286 | * @param isSelectionEnabled - Whether block selection should be enabled. 287 | */ 288 | export function toggleSelection( isSelectionEnabled?: boolean ): void; 289 | 290 | /** 291 | * Signals that the block with the specified client ID has been updated. 292 | * 293 | * @param clientId - Block client ID. 294 | * @param updates - Block attributes to be merged. 295 | */ 296 | export function updateBlock( 297 | clientId: string, 298 | updates: Partial< BlockInstance > 299 | ): void; 300 | 301 | /** 302 | * Signals that the block attributes with the specified client ID has been updated. 303 | * 304 | * @param clientId - Block client ID. 305 | * @param attributes - Block attributes to be merged. 306 | */ 307 | export function updateBlockAttributes( 308 | clientId: string, 309 | attributes: Record< string, any > 310 | ): void; 311 | 312 | /** 313 | * Changes the nested settings of a given block. 314 | * 315 | * @param clientId - Client ID of the block whose nested setting are being received. 316 | * @param settings - Object with the new settings for the nested block. 317 | */ 318 | export function updateBlockListSettings( 319 | clientId: string, 320 | settings: Record< string, unknown > 321 | ): void; 322 | 323 | /** 324 | * Signals that the block editor settings have been updated. 325 | * 326 | * @param settings - Updated settings. 327 | */ 328 | export function updateSettings( settings: Record< string, unknown > ): void; 329 | 330 | /** 331 | * Signals that the next block change should be marked explicitly as not persistent. 332 | */ 333 | export function __unstableMarkNextChangeAsNotPersistent(): void; 334 | 335 | /** 336 | * Signals that the last block change should be marked explicitly as not persistent. 337 | */ 338 | export function __unstableMarkLastChangeAsPersistent(): void; 339 | -------------------------------------------------------------------------------- /patches/react-autosize-textarea+7.1.0.patch: -------------------------------------------------------------------------------- 1 | diff --git a/node_modules/react-autosize-textarea/lib/TextareaAutosize.d.ts b/node_modules/react-autosize-textarea/lib/TextareaAutosize.d.ts 2 | index ad6ff3f..e0a6d51 100644 3 | --- a/node_modules/react-autosize-textarea/lib/TextareaAutosize.d.ts 4 | +++ b/node_modules/react-autosize-textarea/lib/TextareaAutosize.d.ts 5 | @@ -22,7 +22,7 @@ export declare namespace TextareaAutosize { 6 | lineHeight: number | null; 7 | }; 8 | } 9 | -export declare const TextareaAutosize: React.ForwardRefExoticComponent, "default" | "max" | "required" | "media" | "hidden" | "cite" | "data" | "dir" | "form" | "label" | "slot" | "span" | "style" | "title" | "pattern" | "async" | "start" | "low" | "high" | "defer" | "open" | "disabled" | "color" | "content" | "size" | "wrap" | "multiple" | "summary" | "className" | "height" | "id" | "lang" | "method" | "min" | "name" | "target" | "type" | "width" | "role" | "tabIndex" | "crossOrigin" | "href" | "aria-activedescendant" | "aria-atomic" | "aria-autocomplete" | "aria-busy" | "aria-checked" | "aria-colcount" | "aria-colindex" | "aria-colspan" | "aria-controls" | "aria-current" | "aria-describedby" | "aria-details" | "aria-disabled" | "aria-dropeffect" | "aria-errormessage" | "aria-expanded" | "aria-flowto" | "aria-grabbed" | "aria-haspopup" | "aria-hidden" | "aria-invalid" | "aria-keyshortcuts" | "aria-label" | "aria-labelledby" | "aria-level" | "aria-live" | "aria-modal" | "aria-multiline" | "aria-multiselectable" | "aria-orientation" | "aria-owns" | "aria-placeholder" | "aria-posinset" | "aria-pressed" | "aria-readonly" | "aria-relevant" | "aria-required" | "aria-roledescription" | "aria-rowcount" | "aria-rowindex" | "aria-rowspan" | "aria-selected" | "aria-setsize" | "aria-sort" | "aria-valuemax" | "aria-valuemin" | "aria-valuenow" | "aria-valuetext" | "children" | "dangerouslySetInnerHTML" | "onCopy" | "onCopyCapture" | "onCut" | "onCutCapture" | "onPaste" | "onPasteCapture" | "onCompositionEnd" | "onCompositionEndCapture" | "onCompositionStart" | "onCompositionStartCapture" | "onCompositionUpdate" | "onCompositionUpdateCapture" | "onFocus" | "onFocusCapture" | "onBlur" | "onBlurCapture" | "onChange" | "onChangeCapture" | "onBeforeInput" | "onBeforeInputCapture" | "onInput" | "onInputCapture" | "onReset" | "onResetCapture" | "onSubmit" | "onSubmitCapture" | "onInvalid" | "onInvalidCapture" | "onLoad" | "onLoadCapture" | "onError" | "onErrorCapture" | "onKeyDown" | "onKeyDownCapture" | "onKeyPress" | "onKeyPressCapture" | "onKeyUp" | "onKeyUpCapture" | "onAbort" | "onAbortCapture" | "onCanPlay" | "onCanPlayCapture" | "onCanPlayThrough" | "onCanPlayThroughCapture" | "onDurationChange" | "onDurationChangeCapture" | "onEmptied" | "onEmptiedCapture" | "onEncrypted" | "onEncryptedCapture" | "onEnded" | "onEndedCapture" | "onLoadedData" | "onLoadedDataCapture" | "onLoadedMetadata" | "onLoadedMetadataCapture" | "onLoadStart" | "onLoadStartCapture" | "onPause" | "onPauseCapture" | "onPlay" | "onPlayCapture" | "onPlaying" | "onPlayingCapture" | "onProgress" | "onProgressCapture" | "onRateChange" | "onRateChangeCapture" | "onSeeked" | "onSeekedCapture" | "onSeeking" | "onSeekingCapture" | "onStalled" | "onStalledCapture" | "onSuspend" | "onSuspendCapture" | "onTimeUpdate" | "onTimeUpdateCapture" | "onVolumeChange" | "onVolumeChangeCapture" | "onWaiting" | "onWaitingCapture" | "onAuxClick" | "onAuxClickCapture" | "onClick" | "onClickCapture" | "onContextMenu" | "onContextMenuCapture" | "onDoubleClick" | "onDoubleClickCapture" | "onDrag" | "onDragCapture" | "onDragEnd" | "onDragEndCapture" | "onDragEnter" | "onDragEnterCapture" | "onDragExit" | "onDragExitCapture" | "onDragLeave" | "onDragLeaveCapture" | "onDragOver" | "onDragOverCapture" | "onDragStart" | "onDragStartCapture" | "onDrop" | "onDropCapture" | "onMouseDown" | "onMouseDownCapture" | "onMouseEnter" | "onMouseLeave" | "onMouseMove" | "onMouseMoveCapture" | "onMouseOut" | "onMouseOutCapture" | "onMouseOver" | "onMouseOverCapture" | "onMouseUp" | "onMouseUpCapture" | "onSelect" | "onSelectCapture" | "onTouchCancel" | "onTouchCancelCapture" | "onTouchEnd" | "onTouchEndCapture" | "onTouchMove" | "onTouchMoveCapture" | "onTouchStart" | "onTouchStartCapture" | "onPointerDown" | "onPointerDownCapture" | "onPointerMove" | "onPointerMoveCapture" | "onPointerUp" | "onPointerUpCapture" | "onPointerCancel" | "onPointerCancelCapture" | "onPointerEnter" | "onPointerEnterCapture" | "onPointerLeave" | "onPointerLeaveCapture" | "onPointerOver" | "onPointerOverCapture" | "onPointerOut" | "onPointerOutCapture" | "onGotPointerCapture" | "onGotPointerCaptureCapture" | "onLostPointerCapture" | "onLostPointerCaptureCapture" | "onScroll" | "onScrollCapture" | "onWheel" | "onWheelCapture" | "onAnimationStart" | "onAnimationStartCapture" | "onAnimationEnd" | "onAnimationEndCapture" | "onAnimationIteration" | "onAnimationIterationCapture" | "onTransitionEnd" | "onTransitionEndCapture" | "key" | "classID" | "useMap" | "wmode" | "defaultChecked" | "defaultValue" | "suppressContentEditableWarning" | "suppressHydrationWarning" | "accessKey" | "contentEditable" | "contextMenu" | "draggable" | "placeholder" | "spellCheck" | "translate" | "radioGroup" | "about" | "datatype" | "inlist" | "prefix" | "property" | "resource" | "typeof" | "vocab" | "autoCapitalize" | "autoCorrect" | "autoSave" | "itemProp" | "itemScope" | "itemType" | "itemID" | "itemRef" | "results" | "security" | "unselectable" | "inputMode" | "is" | "kind" | "src" | "srcLang" | "value" | "download" | "hrefLang" | "rel" | "alt" | "coords" | "shape" | "autoPlay" | "controls" | "loop" | "mediaGroup" | "muted" | "playsInline" | "preload" | "autoFocus" | "formAction" | "formEncType" | "formMethod" | "formNoValidate" | "formTarget" | "dateTime" | "acceptCharset" | "action" | "autoComplete" | "encType" | "noValidate" | "manifest" | "allowFullScreen" | "allowTransparency" | "frameBorder" | "marginHeight" | "marginWidth" | "sandbox" | "scrolling" | "seamless" | "srcDoc" | "sizes" | "srcSet" | "accept" | "capture" | "checked" | "list" | "maxLength" | "minLength" | "readOnly" | "step" | "challenge" | "keyType" | "keyParams" | "htmlFor" | "as" | "integrity" | "charSet" | "httpEquiv" | "optimum" | "reversed" | "selected" | "nonce" | "scoped" | "cellPadding" | "cellSpacing" | "colSpan" | "headers" | "rowSpan" | "scope" | "cols" | "rows" | "poster"> & { 10 | +export declare const TextareaAutosize: React.ForwardRefExoticComponent, "default" | "max" | "required" | "media" | "hidden" | "cite" | "data" | "dir" | "form" | "label" | "slot" | "span" | "style" | "title" | "pattern" | "async" | "start" | "low" | "high" | "defer" | "open" | "disabled" | "color" | "content" | "size" | "wrap" | "multiple" | "summary" | "className" | "height" | "id" | "lang" | "method" | "min" | "name" | "target" | "type" | "width" | "role" | "tabIndex" | "crossOrigin" | "href" | "aria-activedescendant" | "aria-atomic" | "aria-autocomplete" | "aria-busy" | "aria-checked" | "aria-colcount" | "aria-colindex" | "aria-colspan" | "aria-controls" | "aria-current" | "aria-describedby" | "aria-details" | "aria-disabled" | "aria-dropeffect" | "aria-errormessage" | "aria-expanded" | "aria-flowto" | "aria-grabbed" | "aria-haspopup" | "aria-hidden" | "aria-invalid" | "aria-keyshortcuts" | "aria-label" | "aria-labelledby" | "aria-level" | "aria-live" | "aria-modal" | "aria-multiline" | "aria-multiselectable" | "aria-orientation" | "aria-owns" | "aria-placeholder" | "aria-posinset" | "aria-pressed" | "aria-readonly" | "aria-relevant" | "aria-required" | "aria-roledescription" | "aria-rowcount" | "aria-rowindex" | "aria-rowspan" | "aria-selected" | "aria-setsize" | "aria-sort" | "aria-valuemax" | "aria-valuemin" | "aria-valuenow" | "aria-valuetext" | "children" | "dangerouslySetInnerHTML" | "onCopy" | "onCopyCapture" | "onCut" | "onCutCapture" | "onPaste" | "onPasteCapture" | "onCompositionEnd" | "onCompositionEndCapture" | "onCompositionStart" | "onCompositionStartCapture" | "onCompositionUpdate" | "onCompositionUpdateCapture" | "onFocus" | "onFocusCapture" | "onBlur" | "onBlurCapture" | "onChange" | "onChangeCapture" | "onBeforeInput" | "onBeforeInputCapture" | "onInput" | "onInputCapture" | "onReset" | "onResetCapture" | "onSubmit" | "onSubmitCapture" | "onInvalid" | "onInvalidCapture" | "onLoad" | "onLoadCapture" | "onError" | "onErrorCapture" | "onKeyDown" | "onKeyDownCapture" | "onKeyPress" | "onKeyPressCapture" | "onKeyUp" | "onKeyUpCapture" | "onAbort" | "onAbortCapture" | "onCanPlay" | "onCanPlayCapture" | "onCanPlayThrough" | "onCanPlayThroughCapture" | "onDurationChange" | "onDurationChangeCapture" | "onEmptied" | "onEmptiedCapture" | "onEncrypted" | "onEncryptedCapture" | "onEnded" | "onEndedCapture" | "onLoadedData" | "onLoadedDataCapture" | "onLoadedMetadata" | "onLoadedMetadataCapture" | "onLoadStart" | "onLoadStartCapture" | "onPause" | "onPauseCapture" | "onPlay" | "onPlayCapture" | "onPlaying" | "onPlayingCapture" | "onProgress" | "onProgressCapture" | "onRateChange" | "onRateChangeCapture" | "onSeeked" | "onSeekedCapture" | "onSeeking" | "onSeekingCapture" | "onStalled" | "onStalledCapture" | "onSuspend" | "onSuspendCapture" | "onTimeUpdate" | "onTimeUpdateCapture" | "onVolumeChange" | "onVolumeChangeCapture" | "onWaiting" | "onWaitingCapture" | "onAuxClick" | "onAuxClickCapture" | "onClick" | "onClickCapture" | "onContextMenu" | "onContextMenuCapture" | "onDoubleClick" | "onDoubleClickCapture" | "onDrag" | "onDragCapture" | "onDragEnd" | "onDragEndCapture" | "onDragEnter" | "onDragEnterCapture" | "onDragExit" | "onDragExitCapture" | "onDragLeave" | "onDragLeaveCapture" | "onDragOver" | "onDragOverCapture" | "onDragStart" | "onDragStartCapture" | "onDrop" | "onDropCapture" | "onMouseDown" | "onMouseDownCapture" | "onMouseEnter" | "onMouseLeave" | "onMouseMove" | "onMouseMoveCapture" | "onMouseOut" | "onMouseOutCapture" | "onMouseOver" | "onMouseOverCapture" | "onMouseUp" | "onMouseUpCapture" | "onSelect" | "onSelectCapture" | "onTouchCancel" | "onTouchCancelCapture" | "onTouchEnd" | "onTouchEndCapture" | "onTouchMove" | "onTouchMoveCapture" | "onTouchStart" | "onTouchStartCapture" | "onPointerDown" | "onPointerDownCapture" | "onPointerMove" | "onPointerMoveCapture" | "onPointerUp" | "onPointerUpCapture" | "onPointerCancel" | "onPointerCancelCapture" | "onPointerEnter" | "onPointerLeave" | "onPointerOver" | "onPointerOverCapture" | "onPointerOut" | "onPointerOutCapture" | "onGotPointerCapture" | "onGotPointerCaptureCapture" | "onLostPointerCapture" | "onLostPointerCaptureCapture" | "onScroll" | "onScrollCapture" | "onWheel" | "onWheelCapture" | "onAnimationStart" | "onAnimationStartCapture" | "onAnimationEnd" | "onAnimationEndCapture" | "onAnimationIteration" | "onAnimationIterationCapture" | "onTransitionEnd" | "onTransitionEndCapture" | "key" | "classID" | "useMap" | "wmode" | "defaultChecked" | "defaultValue" | "suppressContentEditableWarning" | "suppressHydrationWarning" | "accessKey" | "contentEditable" | "contextMenu" | "draggable" | "placeholder" | "spellCheck" | "translate" | "radioGroup" | "about" | "datatype" | "inlist" | "prefix" | "property" | "resource" | "typeof" | "vocab" | "autoCapitalize" | "autoCorrect" | "autoSave" | "itemProp" | "itemScope" | "itemType" | "itemID" | "itemRef" | "results" | "security" | "unselectable" | "inputMode" | "is" | "kind" | "src" | "srcLang" | "value" | "download" | "hrefLang" | "rel" | "alt" | "coords" | "shape" | "autoPlay" | "controls" | "loop" | "mediaGroup" | "muted" | "playsInline" | "preload" | "autoFocus" | "formAction" | "formEncType" | "formMethod" | "formNoValidate" | "formTarget" | "dateTime" | "acceptCharset" | "action" | "autoComplete" | "encType" | "noValidate" | "manifest" | "allowFullScreen" | "allowTransparency" | "frameBorder" | "marginHeight" | "marginWidth" | "sandbox" | "scrolling" | "seamless" | "srcDoc" | "sizes" | "srcSet" | "accept" | "capture" | "checked" | "list" | "maxLength" | "minLength" | "readOnly" | "step" | "challenge" | "keyType" | "keyParams" | "htmlFor" | "as" | "integrity" | "charSet" | "httpEquiv" | "optimum" | "reversed" | "selected" | "nonce" | "scoped" | "cellPadding" | "cellSpacing" | "colSpan" | "headers" | "rowSpan" | "scope" | "cols" | "rows" | "poster"> & { 11 | /** Called whenever the textarea resizes */ 12 | onResize?: ((e: Event) => void) | undefined; 13 | /** Minimum number of visible rows */ 14 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. -------------------------------------------------------------------------------- /inc/functions.php: -------------------------------------------------------------------------------- 1 | __NAMESPACE__ . '\render_summarize_block', 23 | ] 24 | ); 25 | } 26 | 27 | add_action( 'init', __NAMESPACE__ . '\register_summarize_button_block' ); 28 | 29 | /** 30 | * Render callback for the summarize block. 31 | * 32 | * @param array $attributes Block attributes. 33 | * @param string $content Block content. 34 | * @param WP_Block $block Block instance. 35 | * @return string Rendered block type output. 36 | */ 37 | function render_summarize_block( $attributes, string $content, WP_Block $block ) { 38 | wp_enqueue_script_module( '@ai-experiments/summarize-button' ); 39 | wp_enqueue_style( 'ai-experiments-summarize-button' ); 40 | 41 | // TODO: Figure out why the stylesheet is not enqueued the regular way. 42 | wp_add_inline_style( 43 | 'wp-block-library', 44 | file_get_contents( 45 | plugin_dir_path( __DIR__ ) . 'build/style-summarize-button.css' 46 | ) 47 | ); 48 | 49 | $id = wp_generate_uuid4(); 50 | 51 | $popover_button_id = "ai-summary-button=$id"; 52 | $popover_id = "ai-summary-popover=$id"; 53 | 54 | $summary_context = $attributes['context'] ?? 'post'; 55 | 56 | $context = [ 57 | 'summary' => '', 58 | 'summaryContext' => $summary_context, 59 | 'isLoading' => false, 60 | 'isOpen' => false, 61 | 'buttonText' => __( 'Read AI-generated summary', 'ai-experiments' ), 62 | ]; 63 | 64 | ob_start(); 65 | ?> 66 |
68 | 69 | data-wp-interactive="ai-experiments/summarize-button" 70 | > 71 | 94 |
100 | 108 |

109 |
110 |
111 | 'defer', 181 | ) 182 | ); 183 | 184 | wp_set_script_translations( 'ai-experiments-editor', 'ai-experiments' ); 185 | 186 | wp_enqueue_style( 187 | 'ai-experiments-editor', 188 | plugins_url( 'build/style-editor.css', __DIR__ ), 189 | array( 'wp-components' ), 190 | $asset['version'] 191 | ); 192 | 193 | wp_style_add_data( 'media-experiments-editor', 'rtl', 'replace' ); 194 | 195 | $asset_file = dirname( __DIR__ ) . '/build/summarize-button.asset.php'; 196 | $asset = is_readable( $asset_file ) ? require $asset_file : []; 197 | 198 | $asset['dependencies'] = $asset['dependencies'] ?? []; 199 | $asset['version'] = $asset['version'] ?? ''; 200 | 201 | wp_enqueue_script( 202 | 'ai-experiments-summarize-button', 203 | plugins_url( 'build/summarize-button.js', __DIR__ ), 204 | $asset['dependencies'], 205 | $asset['version'], 206 | array( 207 | 'strategy' => 'defer', 208 | ) 209 | ); 210 | 211 | $asset_file = dirname( __DIR__ ) . '/build/translate-button.asset.php'; 212 | $asset = is_readable( $asset_file ) ? require $asset_file : []; 213 | 214 | $asset['dependencies'] = $asset['dependencies'] ?? []; 215 | $asset['version'] = $asset['version'] ?? ''; 216 | 217 | wp_enqueue_script( 218 | 'ai-experiments-translate-button', 219 | plugins_url( 'build/translate-button.js', __DIR__ ), 220 | $asset['dependencies'], 221 | $asset['version'], 222 | array( 223 | 'strategy' => 'defer', 224 | ) 225 | ); 226 | } 227 | 228 | add_action( 'enqueue_block_editor_assets', __NAMESPACE__ . '\enqueue_block_editor_assets' ); 229 | 230 | /** 231 | * Enqueues scripts for the block editor, iframed. 232 | * 233 | * @return void 234 | */ 235 | function enqueue_block_assets(): void { 236 | if ( ! is_admin() ) { 237 | return; 238 | } 239 | 240 | $asset_file = dirname( __DIR__ ) . '/build/editor.asset.php'; 241 | $asset = is_readable( $asset_file ) ? require $asset_file : []; 242 | 243 | $asset['dependencies'] = $asset['dependencies'] ?? []; 244 | $asset['version'] = $asset['version'] ?? ''; 245 | 246 | wp_enqueue_style( 247 | 'ai-experiments-editor', 248 | plugins_url( 'build/style-editor.css', __DIR__ ), 249 | array( 'wp-components' ), 250 | $asset['version'] 251 | ); 252 | 253 | wp_style_add_data( 'media-experiments-editor', 'rtl', 'replace' ); 254 | } 255 | 256 | add_action( 'enqueue_block_assets', __NAMESPACE__ . '\enqueue_block_assets' ); 257 | 258 | /** 259 | * Registers assets for the frontend. 260 | */ 261 | function register_frontend_assets() { 262 | $asset_file = dirname( __DIR__ ) . '/build/summarize-button-view.asset.php'; 263 | $asset = is_readable( $asset_file ) ? require $asset_file : []; 264 | 265 | $asset['dependencies'] = $asset['dependencies'] ?? []; 266 | $asset['version'] = $asset['version'] ?? ''; 267 | 268 | wp_register_script_module( 269 | '@ai-experiments/summarize-button', 270 | plugins_url( 'build/summarize-button-view.js', __DIR__ ), 271 | array( '@wordpress/interactivity' ), 272 | $asset['version'], 273 | ); 274 | 275 | $asset_file = dirname( __DIR__ ) . '/build/summarize-button.asset.php'; 276 | $asset = is_readable( $asset_file ) ? require $asset_file : []; 277 | 278 | $asset['dependencies'] = $asset['dependencies'] ?? []; 279 | $asset['version'] = $asset['version'] ?? ''; 280 | 281 | wp_register_style( 282 | 'ai-experiments-summarize-button', 283 | plugins_url( 'build/style-summarize-button.css', __DIR__ ), 284 | $asset['dependencies'], 285 | $asset['version'], 286 | ); 287 | 288 | $asset_file = dirname( __DIR__ ) . '/build/translate-button-view.asset.php'; 289 | $asset = is_readable( $asset_file ) ? require $asset_file : []; 290 | 291 | $asset['dependencies'] = $asset['dependencies'] ?? []; 292 | $asset['version'] = $asset['version'] ?? ''; 293 | 294 | wp_register_script_module( 295 | '@ai-experiments/translate-button', 296 | plugins_url( 'build/translate-button-view.js', __DIR__ ), 297 | array( '@wordpress/interactivity' ), 298 | $asset['version'], 299 | ); 300 | 301 | $asset_file = dirname( __DIR__ ) . '/build/translate-button.asset.php'; 302 | $asset = is_readable( $asset_file ) ? require $asset_file : []; 303 | 304 | $asset['dependencies'] = $asset['dependencies'] ?? []; 305 | $asset['version'] = $asset['version'] ?? ''; 306 | 307 | wp_register_style( 308 | 'ai-experiments-translate-button', 309 | plugins_url( 'build/style-translate-button.css', __DIR__ ), 310 | $asset['dependencies'], 311 | $asset['version'], 312 | ); 313 | } 314 | 315 | add_action( 'init', __NAMESPACE__ . '\register_frontend_assets' ); 316 | 317 | 318 | /** 319 | * Registers the translate button block. 320 | */ 321 | function register_translate_button_block() { 322 | register_block_type( 323 | dirname( __DIR__ ) . '/build/translate-button', 324 | [ 325 | 'render_callback' => __NAMESPACE__ . '\render_translate_block', 326 | ] 327 | ); 328 | } 329 | 330 | add_action( 'init', __NAMESPACE__ . '\register_translate_button_block' ); 331 | 332 | /** 333 | * Render callback for the translate block. 334 | * 335 | * @param array $attributes Block attributes. 336 | * @param string $content Block content. 337 | * @param WP_Block $block Block instance. 338 | * @return string Rendered block type output. 339 | */ 340 | function render_translate_block( $attributes, string $content, WP_Block $block ) { 341 | wp_enqueue_script_module( '@ai-experiments/translate-button' ); 342 | wp_enqueue_style( 'ai-experiments-translate-button' ); 343 | 344 | // TODO: Figure out why the stylesheet is not enqueued the regular way. 345 | // TODO: Only load it once. 346 | wp_add_inline_style( 347 | 'wp-block-library', 348 | file_get_contents( 349 | plugin_dir_path( __DIR__ ) . 'build/style-translate-button.css' 350 | ) 351 | ); 352 | 353 | $context = [ 354 | 'isTranslatable' => false, 355 | 'isLoading' => false, 356 | 'isShowingTranslation' => false, 357 | 'translation' => null, 358 | 'commentId' => isset( $block->context['commentId'] ) ? (int) $block->context['commentId'] : null, 359 | 'buttonText' => __( 'Translate', 'ai-experiments' ), 360 | 'sourceLanguage' => explode( '_', get_locale() )[0], 361 | 'targetLanguage' => explode( '_', get_locale() )[0], 362 | ]; 363 | 364 | ob_start(); 365 | ?> 366 |
368 | 369 | data-wp-interactive="ai-experiments/translate-button" 370 | > 371 | 393 |
394 |
395 | ( 24 | 31 | 32 | 33 | ); 34 | 35 | const LabelAutoIcon = () => ( 36 | 43 | 44 | 45 | ); 46 | 47 | const LightBulbTipsIcon = () => ( 48 | 55 | 56 | 57 | ); 58 | 59 | const MAX_TERMS_SUGGESTIONS = 20; 60 | const DEFAULT_QUERY = { 61 | per_page: MAX_TERMS_SUGGESTIONS, 62 | _fields: 'id,name', 63 | context: 'view', 64 | }; 65 | 66 | function useAICommandLoader() { 67 | const { editPost } = useDispatch( editorStore ); 68 | const { 69 | replaceBlock, 70 | insertBlock, 71 | __unstableMarkNextChangeAsNotPersistent, 72 | __unstableMarkLastChangeAsPersistent, 73 | } = useDispatch( blockEditorStore ); 74 | 75 | const [ isLoading ] = useState( false ); 76 | 77 | const { blocks, plainTextContent } = useSelect( ( select ) => { 78 | const allBlocks: BlockInstance[] = select( blockEditorStore ) 79 | // @ts-ignore 80 | .getClientIdsWithDescendants() 81 | .map( ( clientId: string ) => 82 | // @ts-ignore 83 | select( blockEditorStore ).getBlock( clientId ) 84 | ) 85 | .filter( ( block ) => block !== null ); 86 | 87 | // @ts-ignore 88 | const content: string = 89 | new window.DOMParser().parseFromString( 90 | serialize( allBlocks ), 91 | 'text/html' 92 | ).body.textContent || ''; 93 | return { 94 | blocks: allBlocks, 95 | plainTextContent: content, 96 | }; 97 | }, [] ); 98 | 99 | const { termsList } = useSelect( ( select ) => { 100 | // @ts-ignore 101 | const { getEntityRecords, getTaxonomies } = select( coreStore ); 102 | 103 | // @ts-ignore 104 | const postType = select( editorStore ).getCurrentPostType(); 105 | 106 | // @ts-ignore 107 | const _taxonomies = getTaxonomies( { 108 | per_page: -1, 109 | } ); 110 | const visibleTaxonomies = ( _taxonomies ?? [] ).filter( 111 | ( taxonomy: { 112 | types: string[]; 113 | visibility?: { show_ui: boolean }; 114 | } ) => 115 | // In some circumstances .visibility can end up as undefined so optional chaining operator required. 116 | // https://github.com/WordPress/gutenberg/issues/40326 117 | taxonomy.types.includes( postType ) && 118 | taxonomy.visibility?.show_ui 119 | ); 120 | 121 | const termsPerTaxonomy = Object.fromEntries( 122 | visibleTaxonomies.map( ( { slug }: { slug: string } ) => { 123 | return [ 124 | slug, 125 | // @ts-ignore 126 | getEntityRecords( 127 | 'taxonomy', 128 | slug, 129 | DEFAULT_QUERY 130 | ) as Term[], 131 | ]; 132 | } ) 133 | ); 134 | 135 | // eslint-disable-next-line @typescript-eslint/no-shadow 136 | const termsList = ( 137 | visibleTaxonomies?.map( ( { slug }: { slug: string } ) => { 138 | return `Taxonomy: ${ slug }:\n\n${ 139 | termsPerTaxonomy[ slug ] 140 | ?.map( 141 | ( { 142 | name: term, 143 | id, 144 | }: { 145 | name: string; 146 | id: number; 147 | } ) => `* ${ term } (ID: ${ id })` 148 | ) 149 | .join( '\n' ) || '' 150 | }`; 151 | } ) || [] 152 | ).join( '\n\n' ); 153 | 154 | return { 155 | termsList, 156 | }; 157 | }, [] ); 158 | 159 | const commands = [ 160 | { 161 | name: 'ai-experiments/write-excerpt', 162 | label: __( 'Write excerpt', 'ai-experiments' ), 163 | icon: , 164 | // @ts-ignore 165 | callback: async ( { close } ) => { 166 | close(); 167 | 168 | const session = await LanguageModel.create(); 169 | 170 | const stream = session.promptStreaming( 171 | `Summarise the following text in full sentences in less than 300 characters: ${ plainTextContent }` 172 | ); 173 | 174 | let result = ''; 175 | 176 | for await ( const value of stream ) { 177 | // Each result contains the full data, not just the incremental part. 178 | result = value; 179 | 180 | editPost( { excerpt: result }, { isCached: true } ); 181 | } 182 | 183 | result = result.replaceAll( '\n\n\n\n', '\n\n' ); 184 | 185 | editPost( { excerpt: result } ); 186 | }, 187 | }, 188 | { 189 | name: 'ai-experiments/write-title', 190 | label: __( 'Write title', 'ai-experiments' ), 191 | icon: , 192 | // @ts-ignore 193 | callback: async ( { close } ) => { 194 | close(); 195 | 196 | // const summarizer = await wSummarizer.create( { 197 | // sharedContext: 'A blog post', 198 | // type: 'headline', 199 | // format: 'plain-text', 200 | // length: 'medium', 201 | // } ); 202 | // 203 | // const stream = summarizer.summarizeStreaming( 204 | // plainTextContent.slice( 0, 1000 ), 205 | // { 206 | // context: 207 | // 'Avoid any toxic language and be as constructive as possible.', 208 | // } 209 | // ); 210 | 211 | const session = await LanguageModel.create(); 212 | 213 | const stream = session.promptStreaming( 214 | `Write a plain-text headline for the following text in less than 100 characters: ${ plainTextContent }` 215 | ); 216 | 217 | let result = ''; 218 | 219 | for await ( const value of stream ) { 220 | // Each result contains the full data, not just the incremental part. 221 | result = value; 222 | } 223 | 224 | result = result.trim(); 225 | result = result.replaceAll( /\.$/g, '' ); 226 | 227 | editPost( { title: result } ); 228 | }, 229 | }, 230 | { 231 | name: 'ai-experiments/generate-permalink', 232 | label: __( 'Generate permalink', 'ai-experiments' ), 233 | icon: , 234 | // @ts-ignore 235 | callback: async ( { close } ) => { 236 | close(); 237 | 238 | const session = await LanguageModel.create( { 239 | initialPrompts: [ 240 | { 241 | role: 'system', 242 | content: 243 | 'Write a plain-text URL slug for for the following text in less than 100 character', 244 | }, 245 | { 246 | role: 'user', 247 | content: 248 | 'At WordCamp US 2024 I gave a presentation about client-side media processing, which is all about bringing WordPress’ media uploading and editing capabilities from the server to the browser. The recording is not yet available, but in the meantime you can re-watch the livestream or check out the slides. This blog post is a written adaption of this talk.', 249 | }, 250 | { 251 | role: 'assistant', 252 | content: 'wordpress-media-processing', 253 | }, 254 | { 255 | role: 'user', 256 | content: 257 | 'Learn how to leverage WordPress Playground and Blueprints for automated end-to-end browser and performance testing.', 258 | }, 259 | { 260 | role: 'assistant', 261 | content: 'wordpress-playground-testing', 262 | }, 263 | ], 264 | } ); 265 | 266 | const stream = session.promptStreaming( 267 | ` ${ plainTextContent }` 268 | ); 269 | 270 | let result = ''; 271 | 272 | for await ( const value of stream ) { 273 | // Each result contains the full data, not just the incremental part. 274 | result = value; 275 | } 276 | 277 | result = result.trim(); 278 | result = result.replaceAll( /\.$/g, '' ); 279 | 280 | editPost( { slug: result } ); 281 | }, 282 | }, 283 | { 284 | name: 'ai-experiments/assign-tags', 285 | label: __( 'Assign tags and categories', 'ai-experiments' ), 286 | icon: , 287 | // @ts-ignore 288 | callback: async ( { close } ) => { 289 | close(); 290 | 291 | const session = await LanguageModel.create( { 292 | initialPrompts: [ 293 | { 294 | role: 'system', 295 | content: `You are a content assistant tasked with categorizing given content with the correct terms. 296 | 297 | The following terms exist on the site: 298 | 299 | ${ termsList } 300 | 301 | Given the provided content, determine the most suitable terms to describe the content. 302 | Do not summarize the content. Do not hallucinate. 303 | Provide the output as a comma-separated list of recommended term IDs. 304 | `, 305 | }, 306 | { 307 | role: 'user', 308 | content: 309 | 'This is a presentation about my favorite content management system, WordPress. Go check it out.', 310 | }, 311 | { 312 | role: 'assistant', 313 | content: '10,6', 314 | }, 315 | { 316 | role: 'user', 317 | content: `I love pizza and Drupal!`, 318 | }, 319 | { 320 | role: 'assistant', 321 | content: '1,3,4', 322 | }, 323 | ], 324 | } ); 325 | 326 | const prompt = 327 | `You are a content assistant tasked with categorizing given content with the correct terms. 328 | 329 | The following terms exist on the site: 330 | 331 | ${ termsList } 332 | 333 | Given these terms and provided content, determine the most suitable terms to describe the content. 334 | Do not summarize. Do not hallucinate. 335 | Provide the output as a comma-separated list of numeric term IDs. 336 | 337 | Content: 338 | 339 | ${ plainTextContent }` 340 | .replaceAll( '\t', '' ) 341 | .replaceAll( '\n\n\n\n', '\n\n' ); 342 | 343 | const stream = session.promptStreaming( prompt ); 344 | 345 | let result = ''; 346 | 347 | for await ( const value of stream ) { 348 | // Each result contains the full data, not just the incremental part. 349 | result = value; 350 | } 351 | 352 | result = result.replaceAll( '\n\n\n\n', '\n\n' ); 353 | 354 | const newTermIds = result 355 | .split( ',' ) 356 | .filter( Boolean ) 357 | .map( ( value ) => Number( value.trim() ) ) 358 | .filter( ( value ) => ! Number.isNaN( value ) ); 359 | 360 | // TODO: Map term IDs back to taxonomy rest_base. 361 | editPost( { tags: newTermIds } ); 362 | }, 363 | }, 364 | { 365 | name: 'ai-experiments/sentiment', 366 | label: __( 'Sentiment analysis', 'ai-experiments' ), 367 | icon: , 368 | // @ts-ignore 369 | callback: async ( { close } ) => { 370 | close(); 371 | 372 | const session = await LanguageModel.create(); 373 | 374 | const stream = session.promptStreaming( 375 | `What is the overall vibe of this content? Only respond with "positive" or "negative". Do not provide any explanation for your answer. ${ plainTextContent }` 376 | ); 377 | 378 | let result = ''; 379 | 380 | for await ( const value of stream ) { 381 | // Each result contains the full data, not just the incremental part. 382 | result = value; 383 | } 384 | 385 | // eslint-disable-next-line no-alert -- For testing only. 386 | alert( result ); 387 | }, 388 | }, 389 | { 390 | name: 'ai-experiments/generate-quote', 391 | label: __( 'Generate memorable quote', 'ai-experiments' ), 392 | icon: , 393 | // @ts-ignore 394 | callback: async ( { close } ) => { 395 | close(); 396 | 397 | const session = await LanguageModel.create(); 398 | 399 | const stream = session.promptStreaming( 400 | `You are a writing assistant tasked with providing feedback on content and rephrasing texts to make them more readable and contain less errors. From the following user-provided text, extract a short, memorable quote that can be easily shared in a tweet. The text is in English. Use English (US) grammar. Do not make spelling mistakes. Here is the text: 401 | ${ plainTextContent }` 402 | ); 403 | 404 | let result = ''; 405 | 406 | const quoteBlock = createBlock( 'core/quote', {}, [ 407 | createBlock( 'core/paragraph', { 408 | content: __( 'Generating…', 'ai-experiments' ), 409 | } ), 410 | ] ); 411 | let newClientId = quoteBlock.clientId; 412 | 413 | insertBlock( quoteBlock, 0 ); 414 | 415 | for await ( const value of stream ) { 416 | // Each result contains the full data, not just the incremental part. 417 | result = value.replaceAll( '\n\n\n\n', '\n\n' ); 418 | result = `
${ result }
`; 419 | 420 | void __unstableMarkNextChangeAsNotPersistent(); 421 | 422 | const parsedBlocks = pasteHandler( { 423 | plainText: result, 424 | mode: 'BLOCKS', 425 | } ); 426 | if ( parsedBlocks.length > 0 ) { 427 | replaceBlock( newClientId, parsedBlocks ); 428 | newClientId = parsedBlocks[ 0 ].clientId; 429 | } 430 | } 431 | 432 | void __unstableMarkLastChangeAsPersistent(); 433 | }, 434 | }, 435 | { 436 | name: 'ai-experiments/translate', 437 | label: __( 'Translate content', 'ai-experiments' ), 438 | icon: language, 439 | // @ts-ignore 440 | callback: async ( { close } ) => { 441 | close(); 442 | 443 | // Translate all blocks separately in parallel. 444 | await Promise.allSettled( 445 | blocks.map( async ( block: BlockInstance ) => { 446 | if ( [ 'core/code' ].includes( block.name ) ) { 447 | return; 448 | } 449 | 450 | const blockContent = getBlockContent( block ); 451 | 452 | const result = await translateString( 453 | blockContent, 454 | 'es' 455 | ); 456 | 457 | if ( null === result ) { 458 | return; 459 | } 460 | 461 | const parsedBlocks = pasteHandler( { 462 | plainText: result, 463 | mode: 'BLOCKS', 464 | } ); 465 | 466 | if ( parsedBlocks.length > 0 ) { 467 | void __unstableMarkNextChangeAsNotPersistent(); 468 | replaceBlock( block.clientId, parsedBlocks ); 469 | } 470 | } ) 471 | ); 472 | 473 | void __unstableMarkLastChangeAsPersistent(); 474 | }, 475 | }, 476 | ]; 477 | 478 | return { 479 | commands, 480 | isLoading, 481 | }; 482 | } 483 | 484 | function RenderPlugin() { 485 | useCommandLoader( { 486 | name: 'ai-experiments/ai-commands', 487 | hook: useAICommandLoader, 488 | } ); 489 | return null; 490 | } 491 | 492 | registerPlugin( 'ai-experiments', { 493 | render: RenderPlugin, 494 | } ); 495 | -------------------------------------------------------------------------------- /packages/editor/src/@types/wordpress__block-editor/store/selectors.d.ts: -------------------------------------------------------------------------------- 1 | import type { BlockInstance } from '@wordpress/blocks'; 2 | 3 | /** 4 | * Determines if the given block type is allowed to be inserted into the block list. 5 | * 6 | * @param state 7 | * @param blockName - The name of the block type, e.g.' core/paragraph'. 8 | * @param rootClientId - Optional root client ID of block list. 9 | * 10 | * @returns Whether the given block type is allowed to be inserted. 11 | */ 12 | export function canInsertBlockType( 13 | state: Record< string, unknown >, 14 | blockName: string, 15 | rootClientId?: string 16 | ): boolean; 17 | 18 | /** 19 | * Returns the client ID of the block adjacent one at the given reference `startClientId` and modifier 20 | * directionality. Defaults start `startClientId` to the selected block, and direction as next block. 21 | * Returns `null` if there is no adjacent block. 22 | * 23 | * @param state 24 | * @param startClientId - Optional client ID of block from which to search. 25 | * @param modifier - Directionality multiplier (1 next, -1 previous). 26 | * 27 | * @returns Return the client ID of the block, or null if none exists. 28 | */ 29 | export function getAdjacentBlockClientId( 30 | state: Record< string, unknown >, 31 | startClientId?: string, 32 | modifier?: 1 | -1 33 | ): string | null; 34 | 35 | /** 36 | * Returns a block given its client ID. This is a parsed copy of the block, containing its 37 | * `blockName`, `clientId`, and current `attributes` state. This is not the block's registration 38 | * settings, which must be retrieved from the blocks module registration store. 39 | * 40 | * @param state 41 | * @param clientId - Block client ID. 42 | * 43 | * @returns Parsed block object. 44 | */ 45 | export function getBlock( 46 | state: Record< string, unknown >, 47 | clientId: string 48 | ): BlockInstance | null; 49 | 50 | /** 51 | * Returns a block's attributes given its client ID, or null if no block exists with the client ID. 52 | * 53 | * @param state 54 | * @param clientId - Block client ID. 55 | * 56 | * @returns Block attributes. 57 | */ 58 | export function getBlockAttributes( 59 | state: Record< string, unknown >, 60 | clientId: string 61 | ): Record< string, any > | null; 62 | 63 | /** 64 | * Returns the number of blocks currently present in the post. 65 | * 66 | * @param state 67 | * @param rootClientId - Optional root client ID of block list. 68 | * 69 | * @returns Number of blocks in the post. 70 | */ 71 | export function getBlockCount( 72 | state: Record< string, unknown >, 73 | rootClientId?: string 74 | ): number; 75 | 76 | /** 77 | * Given a block client ID, returns the root of the hierarchy from which the block is nested, return 78 | * the block itself for root level blocks. 79 | * 80 | * @param state 81 | * @param clientId - Block from which to find root client ID. 82 | * 83 | * @returns Root client ID 84 | */ 85 | export function getBlockHierarchyRootClientId( 86 | state: Record< string, unknown >, 87 | clientId: string 88 | ): string; 89 | 90 | /** 91 | * Returns the index at which the block corresponding to the specified client ID occurs within the 92 | * block order, or `-1` if the block does not exist. 93 | * 94 | * @param state 95 | * @param clientId - Block client ID. 96 | * @param rootClientId - Optional root client ID of block list. 97 | * 98 | * @returns Index at which block exists in order. 99 | */ 100 | export function getBlockIndex( 101 | state: Record< string, unknown >, 102 | clientId: string, 103 | rootClientId?: string 104 | ): number; 105 | 106 | /** 107 | * Returns the insertion point, the index at which the new inserted block would 108 | * be placed. Defaults to the last index. 109 | */ 110 | export function getBlockInsertionPoint(): { 111 | state: Record< string, unknown >; 112 | index: number; 113 | rootClientId?: string | undefined; 114 | }; 115 | 116 | /** 117 | * Returns the Block List settings of a block, if any exist. 118 | * 119 | * @param state 120 | * @param clientId - Block client ID. 121 | * 122 | * @returns Block settings of the block if set. 123 | */ 124 | export function getBlockListSettings( 125 | state: Record< string, unknown >, 126 | clientId?: string 127 | ): Record< string, unknown > | undefined; 128 | 129 | /** 130 | * Returns the block's editing mode, defaulting to `"visual"` if not explicitly assigned. 131 | * 132 | * @param state 133 | * @param clientId - Block client ID. 134 | * 135 | * @returns Block editing mode. 136 | */ 137 | export function getBlockMode( 138 | state: Record< string, unknown >, 139 | clientId: string 140 | ): string; 141 | 142 | /** 143 | * Returns a block's name given its client ID, or `null` if no block exists with the client ID. 144 | * 145 | * @param state 146 | * @param clientId - Block client ID. 147 | * 148 | * @returns Block name. 149 | */ 150 | export function getBlockName( 151 | state: Record< string, unknown >, 152 | clientId: string 153 | ): string | null; 154 | 155 | /** 156 | * Returns an array containing all block client IDs in the editor in the order they appear. 157 | * Optionally accepts a root client ID of the block list for which the order should be returned, 158 | * defaulting to the top-level block order. 159 | * 160 | * @param state 161 | * @param rootClientId - Optional root client ID of block list. 162 | * 163 | * @returns Ordered client IDs of editor blocks. 164 | */ 165 | export function getBlockOrder( 166 | state: Record< string, unknown >, 167 | rootClientId?: string 168 | ): string[]; 169 | 170 | /** 171 | * Given a block client ID, returns the root block from which the block is nested, an empty string 172 | * for top-level blocks, or `null` if the block does not exist. 173 | * 174 | * @param state 175 | * @param clientId - Block from which to find root client ID. 176 | * 177 | * @returns Root client ID, if exists 178 | */ 179 | export function getBlockRootClientId( 180 | state: Record< string, unknown >, 181 | clientId: string 182 | ): string | null; 183 | 184 | /** 185 | * Returns the current block selection end. This value may be `undefined`, and it may represent either a 186 | * singular block selection or multi-selection end. A selection is singular if its start and end 187 | * match. 188 | * 189 | * @returns Client ID of block selection end. 190 | */ 191 | export function getBlockSelectionEnd( 192 | state: Record< string, unknown > 193 | ): string | undefined; 194 | 195 | /** 196 | * Returns the current block selection start. This value may be `undefined`, and it may represent 197 | * either a singular block selection or multi-selection start. A selection is singular if its start 198 | * and end match. 199 | * 200 | * @returns Client ID of block selection start. 201 | */ 202 | export function getBlockSelectionStart( 203 | state: Record< string, unknown > 204 | ): string | undefined; 205 | 206 | /** 207 | * Returns all block objects for the current post being edited as an array in the order they appear 208 | * in the post. 209 | * 210 | * Note: It's important to memoize this selector to avoid return a new instance 211 | * on each call 212 | * 213 | * @param state 214 | * @param rootClientId - Optional root client ID of block list. 215 | * 216 | * @returns Post blocks. 217 | */ 218 | export function getBlocks( 219 | state: Record< string, unknown >, 220 | rootClientId?: string 221 | ): BlockInstance[]; 222 | 223 | /** 224 | * Given an array of block client IDs, returns the corresponding array of block objects or `null`. 225 | * 226 | * @param clientIds - Client IDs for which blocks are to be returned. 227 | */ 228 | export function getBlocksByClientId( 229 | clientIds: string | string[] 230 | ): Array< BlockInstance | null >; 231 | 232 | /** 233 | * Returns an array containing the clientIds of all descendants of the blocks given. 234 | * 235 | * @param state 236 | * @param clientIds - Array of block ids to inspect. 237 | * 238 | * @returns ids of descendants. 239 | */ 240 | export function getClientIdsOfDescendants( 241 | state: Record< string, unknown >, 242 | clientIds: string[] 243 | ): string[]; 244 | 245 | /** 246 | * Returns an array containing the `clientIds` of the top-level blocks and their descendants of any 247 | * depth (for nested blocks). 248 | * 249 | * @returns ids of top-level and descendant blocks. 250 | */ 251 | export function getClientIdsWithDescendants( 252 | state: Record< string, unknown > 253 | ): string[]; 254 | 255 | /** 256 | * Returns the client ID of the first block in the multi-selection set, or `null` if there is no 257 | * multi-selection. 258 | * 259 | * @returns First block client ID in the multi-selection set. 260 | */ 261 | export function getFirstMultiSelectedBlockClientId( 262 | state: Record< string, unknown > 263 | ): string | null; 264 | 265 | /** 266 | * Returns the total number of blocks, or the total number of blocks with a specific name in a post. 267 | * The number returned includes nested blocks. 268 | * 269 | * @param state 270 | * @param blockName - Optional block name, if specified only blocks of that type will be counted. 271 | * 272 | * @returns Number of blocks in the post, or number of blocks with name equal to `blockName`. 273 | */ 274 | export function getGlobalBlockCount( 275 | state: Record< string, unknown >, 276 | blockName?: string 277 | ): number; 278 | 279 | /** 280 | * Determines the items that appear in the inserter. Includes both static items (e.g. a regular 281 | * block type) and dynamic items (e.g. a reusable block). 282 | * 283 | * @remarks 284 | * Each item object contains what's necessary to display a button in the inserter and handle its 285 | * selection. 286 | * 287 | * The `utility` property indicates how useful we think an item will be to the user. There are 4 288 | * levels of utility: 289 | * 290 | * 1. Blocks that are contextually useful (utility = 3) 291 | * 2. Blocks that have been previously inserted (utility = 2) 292 | * 3. Blocks that are in the common category (utility = 1) 293 | * 4. All other blocks (utility = 0) 294 | * 295 | * The `frecency` property is a heuristic (https://en.wikipedia.org/wiki/Frecency) 296 | * that combines block usage frequenty and recency. 297 | * 298 | * Items are returned ordered descendingly by their `utility` and `frecency`. 299 | * 300 | * @param state 301 | * @param rootClientId - Optional root client ID of block list. 302 | * 303 | * @returns Items that appear in inserter. 304 | */ 305 | export function getInserterItems( 306 | state: Record< string, unknown >, 307 | rootClientId?: string 308 | ): Record< string, unknown >[]; 309 | 310 | /** 311 | * Returns the client ID of the last block in the multi-selection set, or `null` if there is no 312 | * multi-selection. 313 | * 314 | * @returns Last block client ID in the multi-selection set. 315 | */ 316 | export function getLastMultiSelectedBlockClientId( 317 | state: Record< string, unknown > 318 | ): string | null; 319 | 320 | /** 321 | * Returns the current multi-selection set of block client IDs, or an empty array if there is no 322 | * multi-selection. 323 | * 324 | * @returns Multi-selected block client IDs. 325 | */ 326 | export function getMultiSelectedBlockClientIds( 327 | state: Record< string, unknown > 328 | ): string[]; 329 | 330 | /** 331 | * Returns the current multi-selection set of blocks, or an empty array if there is no 332 | * multi-selection. 333 | * 334 | * @returns Multi-selected block objects. 335 | */ 336 | export function getMultiSelectedBlocks( 337 | state: Record< string, unknown > 338 | ): BlockInstance[]; 339 | 340 | /** 341 | * Returns the client ID of the block which ends the multi-selection set, or `null` if there is no 342 | * multi-selection. 343 | * 344 | * This is not necessarily the last client ID in the selection. 345 | * 346 | * @see getLastMultiSelectedBlockClientId 347 | * 348 | * @returns Client ID of block ending multi-selection. 349 | */ 350 | export function getMultiSelectedBlocksEndClientId( 351 | state: Record< string, unknown > 352 | ): string | null; 353 | 354 | /** 355 | * Returns the client ID of the block which begins the multi-selection set, or `null` if there is no 356 | * multi-selection. 357 | * 358 | * This is not necessarily the first client ID in the selection. 359 | * 360 | * @see getFirstMultiSelectedBlockClientId 361 | * 362 | * @returns Client ID of block beginning multi-selection. 363 | */ 364 | export function getMultiSelectedBlocksStartClientId( 365 | state: Record< string, unknown > 366 | ): string | null; 367 | 368 | /** 369 | * Returns the next block's client ID from the given reference start ID. Defaults start to the 370 | * selected block. Returns `null` if there is no next block. 371 | * 372 | * @param state 373 | * @param startClientId - Optional client ID of block from which to search. 374 | * 375 | * @returns Adjacent block's client ID, or `null` if none exists. 376 | */ 377 | export function getNextBlockClientId( 378 | state: Record< string, unknown >, 379 | startClientId?: string 380 | ): string | null; 381 | 382 | /** 383 | * Returns the previous block's client ID from the given reference start ID. Defaults start to the 384 | * selected block. Returns `null` if there is no previous block. 385 | * 386 | * @param state 387 | * @param startClientId - Optional client ID of block from which to search. 388 | * 389 | * @returns Adjacent block's client ID, or `null` if none exists. 390 | */ 391 | export function getPreviousBlockClientId( 392 | state: Record< string, unknown >, 393 | startClientId?: string 394 | ): string | null; 395 | 396 | /** 397 | * Returns the currently selected block, or `null` if there is no selected block. 398 | * 399 | * @returns Selected block. 400 | */ 401 | export function getSelectedBlock( 402 | state: Record< string, unknown > 403 | ): BlockInstance | null; 404 | 405 | /** 406 | * Returns the currently selected block client ID, or `null` if there is no selected block. 407 | * 408 | * @returns Selected block client ID. 409 | */ 410 | export function getSelectedBlockClientId( 411 | state: Record< string, unknown > 412 | ): string | null; 413 | 414 | /** 415 | * Returns the current selection set of block client IDs (multiselection or single selection). 416 | * 417 | * @returns Multi-selected block client IDs. 418 | */ 419 | export function getSelectedBlockClientIds( 420 | state: Record< string, unknown > 421 | ): string[]; 422 | 423 | /** 424 | * Returns the number of blocks currently selected in the post. 425 | * 426 | * @returns Number of blocks selected in the post. 427 | */ 428 | export function getSelectedBlockCount( 429 | state: Record< string, unknown > 430 | ): number; 431 | 432 | /** 433 | * Returns the initial caret position for the selected block. This position is to used to position 434 | * the caret properly when the selected block changes. 435 | */ 436 | export function getSelectedBlocksInitialCaretPosition( 437 | state: Record< string, unknown > 438 | ): number | null; 439 | 440 | /** 441 | * Returns the current selection end. 442 | * 443 | * @returns Selection end information. 444 | */ 445 | export function getSelectionEnd( 446 | state: Record< string, unknown > 447 | ): Record< string, unknown >; 448 | 449 | /** 450 | * Returns the current selection start. 451 | * 452 | * @returns Selection start information. 453 | */ 454 | export function getSelectionStart( 455 | state: Record< string, unknown > 456 | ): Record< string, unknown >; 457 | 458 | /** 459 | * Returns the editor settings. 460 | */ 461 | export function getSettings( 462 | state: Record< string, unknown > 463 | ): Record< string, unknown >; 464 | 465 | // FIXME: This is poorly documented. It's not clear what this is. 466 | /** 467 | * Returns the defined block template. 468 | */ 469 | export function getTemplate( state: Record< string, unknown > ): any; 470 | 471 | /** 472 | * Returns the defined block template lock. Optionally accepts a root block client ID as context, 473 | * otherwise defaulting to the global context. 474 | * 475 | * @param state 476 | * @param rootClientId - Optional block root client ID. 477 | * 478 | * @returns Block Template Lock 479 | */ 480 | export function getTemplateLock( 481 | state: Record< string, unknown >, 482 | rootClientId?: string 483 | ): string | undefined; 484 | 485 | /** 486 | * Determines whether there are items to show in the inserter. 487 | * 488 | * @param state 489 | * @param rootClientId - Optional root client ID of block list. 490 | * 491 | * @returns Items that appear in inserter. 492 | */ 493 | export function hasInserterItems( 494 | state: Record< string, unknown >, 495 | rootClientId?: string 496 | ): boolean; 497 | 498 | /** 499 | * Returns `true` if a multi-selection has been made, or `false` otherwise. 500 | * 501 | * @returns Whether multi-selection has been made. 502 | */ 503 | export function hasMultiSelection( state: Record< string, unknown > ): boolean; 504 | 505 | /** 506 | * Returns `true` if there is a single selected block, or `false` otherwise. 507 | * 508 | * @returns Whether a single block is selected. 509 | */ 510 | export function hasSelectedBlock( state: Record< string, unknown > ): boolean; 511 | 512 | /** 513 | * Returns `true` if one of the block's inner blocks is selected. 514 | * 515 | * @param state 516 | * @param clientId - Block client ID. 517 | * @param deep - Perform a deep check. (default: `true`) 518 | * 519 | * @returns Whether the block as an inner block selected 520 | */ 521 | export function hasSelectedInnerBlock( 522 | state: Record< string, unknown >, 523 | clientId: string, 524 | deep?: boolean 525 | ): boolean; 526 | 527 | /** 528 | * Returns `true` if an ancestor of the block is multi-selected, or `false` otherwise. 529 | * 530 | * @param state 531 | * @param clientId - Block client ID. 532 | * 533 | * @returns Whether an ancestor of the block is in multi-selection set. 534 | */ 535 | export function isAncestorMultiSelected( 536 | state: Record< string, unknown >, 537 | clientId: string 538 | ): boolean; 539 | 540 | /** 541 | * Returns `true` if we should show the block insertion point. 542 | * 543 | * @returns Whether the insertion point is visible or not. 544 | */ 545 | export function isBlockInsertionPointVisible( 546 | state: Record< string, unknown > 547 | ): boolean; 548 | 549 | /** 550 | * Returns `true` if the client ID occurs within the block multi-selection, or `false` otherwise. 551 | * 552 | * @param state 553 | * @param clientId - Block client ID. 554 | * 555 | * @returns Whether block is in multi-selection set. 556 | */ 557 | export function isBlockMultiSelected( 558 | state: Record< string, unknown >, 559 | clientId: string 560 | ): boolean; 561 | 562 | /** 563 | * Returns `true` if the block corresponding to the specified client ID is currently selected and no 564 | * multi-selection exists, or `false` otherwise. 565 | * 566 | * @param state 567 | * @param clientId - Block client ID. 568 | * 569 | * @returns Whether block is selected and multi-selection exists. 570 | */ 571 | export function isBlockSelected( 572 | state: Record< string, unknown >, 573 | clientId: string 574 | ): boolean; 575 | 576 | /** 577 | * Returns whether a block is valid or not. 578 | * 579 | * @param state 580 | * @param clientId - Block client ID. 581 | * 582 | * @returns Is Valid. 583 | */ 584 | export function isBlockValid( 585 | state: Record< string, unknown >, 586 | clientId: string 587 | ): boolean; 588 | 589 | /** 590 | * Returns `true` if the block corresponding to the specified client ID is currently selected but 591 | * isn't the last of the selected blocks. Here "last" refers to the block sequence in the document, 592 | * _not_ the sequence of multi-selection, which is why `state.blockSelection.end` isn't used. 593 | * 594 | * @param state 595 | * @param clientId - Block client ID. 596 | * 597 | * @returns Whether block is selected and not the last in the selection. 598 | */ 599 | export function isBlockWithinSelection( 600 | state: Record< string, unknown >, 601 | clientId: string 602 | ): boolean; 603 | 604 | /** 605 | * Returns `true` if the caret is within formatted text, or `false` otherwise. 606 | * 607 | * @returns Whether the caret is within formatted text. 608 | */ 609 | export function isCaretWithinFormattedText( 610 | state: Record< string, unknown > 611 | ): boolean; 612 | 613 | /** 614 | * Returns `true` if a multi-selection exists, and the block corresponding to the specified client 615 | * ID is the first block of the multi-selection set, or `false` otherwise. 616 | * 617 | * @param state 618 | * @param clientId - Block client ID. 619 | * 620 | * @returns Whether block is first in multi-selection. 621 | */ 622 | export function isFirstMultiSelectedBlock( 623 | state: Record< string, unknown >, 624 | clientId: string 625 | ): boolean; 626 | 627 | /** 628 | * Returns `true` if the most recent block change is be considered persistent, or `false` otherwise. 629 | * A persistent change is one committed by BlockEditorProvider via its `onChange` callback, in 630 | * addition to `onInput`. 631 | * 632 | * @returns Whether the most recent block change was persistent. 633 | */ 634 | export function isLastBlockChangePersistent( 635 | state: Record< string, unknown > 636 | ): boolean; 637 | 638 | /** 639 | * Whether in the process of multi-selecting or not. This flag is only `true` while the 640 | * multi-selection is being selected (by mouse move), and is `false` once the multi-selection has 641 | * been settled. 642 | * 643 | * @see hasMultiSelection 644 | * 645 | * @returns `true` if multi-selecting, `false` if not. 646 | */ 647 | export function isMultiSelecting( state: Record< string, unknown > ): boolean; 648 | 649 | /** 650 | * Selector that returns if multi-selection is enabled or not. 651 | * 652 | * @returns `true` if it should be possible to multi-select blocks, `false` if multi-selection is 653 | * disabled. 654 | */ 655 | export function isSelectionEnabled( state: Record< string, unknown > ): boolean; 656 | 657 | /** 658 | * Returns `true` if the user is typing, or `false` otherwise. 659 | * 660 | * @returns Whether user is typing. 661 | */ 662 | export function isTyping( state: Record< string, unknown > ): boolean; 663 | 664 | /** 665 | * Returns whether the blocks matches the template or not. 666 | * 667 | * @returns Whether the template is valid or not. 668 | */ 669 | export function isValidTemplate( state: Record< string, unknown > ): boolean; 670 | -------------------------------------------------------------------------------- /packages/summarize-button/src/@types/wordpress__block-editor/store/selectors.d.ts: -------------------------------------------------------------------------------- 1 | import type { BlockInstance } from '@wordpress/blocks'; 2 | 3 | /** 4 | * Determines if the given block type is allowed to be inserted into the block list. 5 | * 6 | * @param state 7 | * @param blockName - The name of the block type, e.g.' core/paragraph'. 8 | * @param rootClientId - Optional root client ID of block list. 9 | * 10 | * @returns Whether the given block type is allowed to be inserted. 11 | */ 12 | export function canInsertBlockType( 13 | state: Record< string, unknown >, 14 | blockName: string, 15 | rootClientId?: string 16 | ): boolean; 17 | 18 | /** 19 | * Returns the client ID of the block adjacent one at the given reference `startClientId` and modifier 20 | * directionality. Defaults start `startClientId` to the selected block, and direction as next block. 21 | * Returns `null` if there is no adjacent block. 22 | * 23 | * @param state 24 | * @param startClientId - Optional client ID of block from which to search. 25 | * @param modifier - Directionality multiplier (1 next, -1 previous). 26 | * 27 | * @returns Return the client ID of the block, or null if none exists. 28 | */ 29 | export function getAdjacentBlockClientId( 30 | state: Record< string, unknown >, 31 | startClientId?: string, 32 | modifier?: 1 | -1 33 | ): string | null; 34 | 35 | /** 36 | * Returns a block given its client ID. This is a parsed copy of the block, containing its 37 | * `blockName`, `clientId`, and current `attributes` state. This is not the block's registration 38 | * settings, which must be retrieved from the blocks module registration store. 39 | * 40 | * @param state 41 | * @param clientId - Block client ID. 42 | * 43 | * @returns Parsed block object. 44 | */ 45 | export function getBlock( 46 | state: Record< string, unknown >, 47 | clientId: string 48 | ): BlockInstance | null; 49 | 50 | /** 51 | * Returns a block's attributes given its client ID, or null if no block exists with the client ID. 52 | * 53 | * @param state 54 | * @param clientId - Block client ID. 55 | * 56 | * @returns Block attributes. 57 | */ 58 | export function getBlockAttributes( 59 | state: Record< string, unknown >, 60 | clientId: string 61 | ): Record< string, any > | null; 62 | 63 | /** 64 | * Returns the number of blocks currently present in the post. 65 | * 66 | * @param state 67 | * @param rootClientId - Optional root client ID of block list. 68 | * 69 | * @returns Number of blocks in the post. 70 | */ 71 | export function getBlockCount( 72 | state: Record< string, unknown >, 73 | rootClientId?: string 74 | ): number; 75 | 76 | /** 77 | * Given a block client ID, returns the root of the hierarchy from which the block is nested, return 78 | * the block itself for root level blocks. 79 | * 80 | * @param state 81 | * @param clientId - Block from which to find root client ID. 82 | * 83 | * @returns Root client ID 84 | */ 85 | export function getBlockHierarchyRootClientId( 86 | state: Record< string, unknown >, 87 | clientId: string 88 | ): string; 89 | 90 | /** 91 | * Returns the index at which the block corresponding to the specified client ID occurs within the 92 | * block order, or `-1` if the block does not exist. 93 | * 94 | * @param state 95 | * @param clientId - Block client ID. 96 | * @param rootClientId - Optional root client ID of block list. 97 | * 98 | * @returns Index at which block exists in order. 99 | */ 100 | export function getBlockIndex( 101 | state: Record< string, unknown >, 102 | clientId: string, 103 | rootClientId?: string 104 | ): number; 105 | 106 | /** 107 | * Returns the insertion point, the index at which the new inserted block would 108 | * be placed. Defaults to the last index. 109 | */ 110 | export function getBlockInsertionPoint(): { 111 | state: Record< string, unknown >; 112 | index: number; 113 | rootClientId?: string | undefined; 114 | }; 115 | 116 | /** 117 | * Returns the Block List settings of a block, if any exist. 118 | * 119 | * @param state 120 | * @param clientId - Block client ID. 121 | * 122 | * @returns Block settings of the block if set. 123 | */ 124 | export function getBlockListSettings( 125 | state: Record< string, unknown >, 126 | clientId?: string 127 | ): Record< string, unknown > | undefined; 128 | 129 | /** 130 | * Returns the block's editing mode, defaulting to `"visual"` if not explicitly assigned. 131 | * 132 | * @param state 133 | * @param clientId - Block client ID. 134 | * 135 | * @returns Block editing mode. 136 | */ 137 | export function getBlockMode( 138 | state: Record< string, unknown >, 139 | clientId: string 140 | ): string; 141 | 142 | /** 143 | * Returns a block's name given its client ID, or `null` if no block exists with the client ID. 144 | * 145 | * @param state 146 | * @param clientId - Block client ID. 147 | * 148 | * @returns Block name. 149 | */ 150 | export function getBlockName( 151 | state: Record< string, unknown >, 152 | clientId: string 153 | ): string | null; 154 | 155 | /** 156 | * Returns an array containing all block client IDs in the editor in the order they appear. 157 | * Optionally accepts a root client ID of the block list for which the order should be returned, 158 | * defaulting to the top-level block order. 159 | * 160 | * @param state 161 | * @param rootClientId - Optional root client ID of block list. 162 | * 163 | * @returns Ordered client IDs of editor blocks. 164 | */ 165 | export function getBlockOrder( 166 | state: Record< string, unknown >, 167 | rootClientId?: string 168 | ): string[]; 169 | 170 | /** 171 | * Given a block client ID, returns the root block from which the block is nested, an empty string 172 | * for top-level blocks, or `null` if the block does not exist. 173 | * 174 | * @param state 175 | * @param clientId - Block from which to find root client ID. 176 | * 177 | * @returns Root client ID, if exists 178 | */ 179 | export function getBlockRootClientId( 180 | state: Record< string, unknown >, 181 | clientId: string 182 | ): string | null; 183 | 184 | /** 185 | * Returns the current block selection end. This value may be `undefined`, and it may represent either a 186 | * singular block selection or multi-selection end. A selection is singular if its start and end 187 | * match. 188 | * 189 | * @returns Client ID of block selection end. 190 | */ 191 | export function getBlockSelectionEnd( 192 | state: Record< string, unknown > 193 | ): string | undefined; 194 | 195 | /** 196 | * Returns the current block selection start. This value may be `undefined`, and it may represent 197 | * either a singular block selection or multi-selection start. A selection is singular if its start 198 | * and end match. 199 | * 200 | * @returns Client ID of block selection start. 201 | */ 202 | export function getBlockSelectionStart( 203 | state: Record< string, unknown > 204 | ): string | undefined; 205 | 206 | /** 207 | * Returns all block objects for the current post being edited as an array in the order they appear 208 | * in the post. 209 | * 210 | * Note: It's important to memoize this selector to avoid return a new instance 211 | * on each call 212 | * 213 | * @param state 214 | * @param rootClientId - Optional root client ID of block list. 215 | * 216 | * @returns Post blocks. 217 | */ 218 | export function getBlocks( 219 | state: Record< string, unknown >, 220 | rootClientId?: string 221 | ): BlockInstance[]; 222 | 223 | /** 224 | * Given an array of block client IDs, returns the corresponding array of block objects or `null`. 225 | * 226 | * @param clientIds - Client IDs for which blocks are to be returned. 227 | */ 228 | export function getBlocksByClientId( 229 | clientIds: string | string[] 230 | ): Array< BlockInstance | null >; 231 | 232 | /** 233 | * Returns an array containing the clientIds of all descendants of the blocks given. 234 | * 235 | * @param state 236 | * @param clientIds - Array of block ids to inspect. 237 | * 238 | * @returns ids of descendants. 239 | */ 240 | export function getClientIdsOfDescendants( 241 | state: Record< string, unknown >, 242 | clientIds: string[] 243 | ): string[]; 244 | 245 | /** 246 | * Returns an array containing the `clientIds` of the top-level blocks and their descendants of any 247 | * depth (for nested blocks). 248 | * 249 | * @returns ids of top-level and descendant blocks. 250 | */ 251 | export function getClientIdsWithDescendants( 252 | state: Record< string, unknown > 253 | ): string[]; 254 | 255 | /** 256 | * Returns the client ID of the first block in the multi-selection set, or `null` if there is no 257 | * multi-selection. 258 | * 259 | * @returns First block client ID in the multi-selection set. 260 | */ 261 | export function getFirstMultiSelectedBlockClientId( 262 | state: Record< string, unknown > 263 | ): string | null; 264 | 265 | /** 266 | * Returns the total number of blocks, or the total number of blocks with a specific name in a post. 267 | * The number returned includes nested blocks. 268 | * 269 | * @param state 270 | * @param blockName - Optional block name, if specified only blocks of that type will be counted. 271 | * 272 | * @returns Number of blocks in the post, or number of blocks with name equal to `blockName`. 273 | */ 274 | export function getGlobalBlockCount( 275 | state: Record< string, unknown >, 276 | blockName?: string 277 | ): number; 278 | 279 | /** 280 | * Determines the items that appear in the inserter. Includes both static items (e.g. a regular 281 | * block type) and dynamic items (e.g. a reusable block). 282 | * 283 | * @remarks 284 | * Each item object contains what's necessary to display a button in the inserter and handle its 285 | * selection. 286 | * 287 | * The `utility` property indicates how useful we think an item will be to the user. There are 4 288 | * levels of utility: 289 | * 290 | * 1. Blocks that are contextually useful (utility = 3) 291 | * 2. Blocks that have been previously inserted (utility = 2) 292 | * 3. Blocks that are in the common category (utility = 1) 293 | * 4. All other blocks (utility = 0) 294 | * 295 | * The `frecency` property is a heuristic (https://en.wikipedia.org/wiki/Frecency) 296 | * that combines block usage frequenty and recency. 297 | * 298 | * Items are returned ordered descendingly by their `utility` and `frecency`. 299 | * 300 | * @param state 301 | * @param rootClientId - Optional root client ID of block list. 302 | * 303 | * @returns Items that appear in inserter. 304 | */ 305 | export function getInserterItems( 306 | state: Record< string, unknown >, 307 | rootClientId?: string 308 | ): Record< string, unknown >[]; 309 | 310 | /** 311 | * Returns the client ID of the last block in the multi-selection set, or `null` if there is no 312 | * multi-selection. 313 | * 314 | * @returns Last block client ID in the multi-selection set. 315 | */ 316 | export function getLastMultiSelectedBlockClientId( 317 | state: Record< string, unknown > 318 | ): string | null; 319 | 320 | /** 321 | * Returns the current multi-selection set of block client IDs, or an empty array if there is no 322 | * multi-selection. 323 | * 324 | * @returns Multi-selected block client IDs. 325 | */ 326 | export function getMultiSelectedBlockClientIds( 327 | state: Record< string, unknown > 328 | ): string[]; 329 | 330 | /** 331 | * Returns the current multi-selection set of blocks, or an empty array if there is no 332 | * multi-selection. 333 | * 334 | * @returns Multi-selected block objects. 335 | */ 336 | export function getMultiSelectedBlocks( 337 | state: Record< string, unknown > 338 | ): BlockInstance[]; 339 | 340 | /** 341 | * Returns the client ID of the block which ends the multi-selection set, or `null` if there is no 342 | * multi-selection. 343 | * 344 | * This is not necessarily the last client ID in the selection. 345 | * 346 | * @see getLastMultiSelectedBlockClientId 347 | * 348 | * @returns Client ID of block ending multi-selection. 349 | */ 350 | export function getMultiSelectedBlocksEndClientId( 351 | state: Record< string, unknown > 352 | ): string | null; 353 | 354 | /** 355 | * Returns the client ID of the block which begins the multi-selection set, or `null` if there is no 356 | * multi-selection. 357 | * 358 | * This is not necessarily the first client ID in the selection. 359 | * 360 | * @see getFirstMultiSelectedBlockClientId 361 | * 362 | * @returns Client ID of block beginning multi-selection. 363 | */ 364 | export function getMultiSelectedBlocksStartClientId( 365 | state: Record< string, unknown > 366 | ): string | null; 367 | 368 | /** 369 | * Returns the next block's client ID from the given reference start ID. Defaults start to the 370 | * selected block. Returns `null` if there is no next block. 371 | * 372 | * @param state 373 | * @param startClientId - Optional client ID of block from which to search. 374 | * 375 | * @returns Adjacent block's client ID, or `null` if none exists. 376 | */ 377 | export function getNextBlockClientId( 378 | state: Record< string, unknown >, 379 | startClientId?: string 380 | ): string | null; 381 | 382 | /** 383 | * Returns the previous block's client ID from the given reference start ID. Defaults start to the 384 | * selected block. Returns `null` if there is no previous block. 385 | * 386 | * @param state 387 | * @param startClientId - Optional client ID of block from which to search. 388 | * 389 | * @returns Adjacent block's client ID, or `null` if none exists. 390 | */ 391 | export function getPreviousBlockClientId( 392 | state: Record< string, unknown >, 393 | startClientId?: string 394 | ): string | null; 395 | 396 | /** 397 | * Returns the currently selected block, or `null` if there is no selected block. 398 | * 399 | * @returns Selected block. 400 | */ 401 | export function getSelectedBlock( 402 | state: Record< string, unknown > 403 | ): BlockInstance | null; 404 | 405 | /** 406 | * Returns the currently selected block client ID, or `null` if there is no selected block. 407 | * 408 | * @returns Selected block client ID. 409 | */ 410 | export function getSelectedBlockClientId( 411 | state: Record< string, unknown > 412 | ): string | null; 413 | 414 | /** 415 | * Returns the current selection set of block client IDs (multiselection or single selection). 416 | * 417 | * @returns Multi-selected block client IDs. 418 | */ 419 | export function getSelectedBlockClientIds( 420 | state: Record< string, unknown > 421 | ): string[]; 422 | 423 | /** 424 | * Returns the number of blocks currently selected in the post. 425 | * 426 | * @returns Number of blocks selected in the post. 427 | */ 428 | export function getSelectedBlockCount( 429 | state: Record< string, unknown > 430 | ): number; 431 | 432 | /** 433 | * Returns the initial caret position for the selected block. This position is to used to position 434 | * the caret properly when the selected block changes. 435 | */ 436 | export function getSelectedBlocksInitialCaretPosition( 437 | state: Record< string, unknown > 438 | ): number | null; 439 | 440 | /** 441 | * Returns the current selection end. 442 | * 443 | * @returns Selection end information. 444 | */ 445 | export function getSelectionEnd( 446 | state: Record< string, unknown > 447 | ): Record< string, unknown >; 448 | 449 | /** 450 | * Returns the current selection start. 451 | * 452 | * @returns Selection start information. 453 | */ 454 | export function getSelectionStart( 455 | state: Record< string, unknown > 456 | ): Record< string, unknown >; 457 | 458 | /** 459 | * Returns the editor settings. 460 | */ 461 | export function getSettings( 462 | state: Record< string, unknown > 463 | ): Record< string, unknown >; 464 | 465 | // FIXME: This is poorly documented. It's not clear what this is. 466 | /** 467 | * Returns the defined block template. 468 | */ 469 | export function getTemplate( state: Record< string, unknown > ): any; 470 | 471 | /** 472 | * Returns the defined block template lock. Optionally accepts a root block client ID as context, 473 | * otherwise defaulting to the global context. 474 | * 475 | * @param state 476 | * @param rootClientId - Optional block root client ID. 477 | * 478 | * @returns Block Template Lock 479 | */ 480 | export function getTemplateLock( 481 | state: Record< string, unknown >, 482 | rootClientId?: string 483 | ): string | undefined; 484 | 485 | /** 486 | * Determines whether there are items to show in the inserter. 487 | * 488 | * @param state 489 | * @param rootClientId - Optional root client ID of block list. 490 | * 491 | * @returns Items that appear in inserter. 492 | */ 493 | export function hasInserterItems( 494 | state: Record< string, unknown >, 495 | rootClientId?: string 496 | ): boolean; 497 | 498 | /** 499 | * Returns `true` if a multi-selection has been made, or `false` otherwise. 500 | * 501 | * @returns Whether multi-selection has been made. 502 | */ 503 | export function hasMultiSelection( state: Record< string, unknown > ): boolean; 504 | 505 | /** 506 | * Returns `true` if there is a single selected block, or `false` otherwise. 507 | * 508 | * @returns Whether a single block is selected. 509 | */ 510 | export function hasSelectedBlock( state: Record< string, unknown > ): boolean; 511 | 512 | /** 513 | * Returns `true` if one of the block's inner blocks is selected. 514 | * 515 | * @param state 516 | * @param clientId - Block client ID. 517 | * @param deep - Perform a deep check. (default: `true`) 518 | * 519 | * @returns Whether the block as an inner block selected 520 | */ 521 | export function hasSelectedInnerBlock( 522 | state: Record< string, unknown >, 523 | clientId: string, 524 | deep?: boolean 525 | ): boolean; 526 | 527 | /** 528 | * Returns `true` if an ancestor of the block is multi-selected, or `false` otherwise. 529 | * 530 | * @param state 531 | * @param clientId - Block client ID. 532 | * 533 | * @returns Whether an ancestor of the block is in multi-selection set. 534 | */ 535 | export function isAncestorMultiSelected( 536 | state: Record< string, unknown >, 537 | clientId: string 538 | ): boolean; 539 | 540 | /** 541 | * Returns `true` if we should show the block insertion point. 542 | * 543 | * @returns Whether the insertion point is visible or not. 544 | */ 545 | export function isBlockInsertionPointVisible( 546 | state: Record< string, unknown > 547 | ): boolean; 548 | 549 | /** 550 | * Returns `true` if the client ID occurs within the block multi-selection, or `false` otherwise. 551 | * 552 | * @param state 553 | * @param clientId - Block client ID. 554 | * 555 | * @returns Whether block is in multi-selection set. 556 | */ 557 | export function isBlockMultiSelected( 558 | state: Record< string, unknown >, 559 | clientId: string 560 | ): boolean; 561 | 562 | /** 563 | * Returns `true` if the block corresponding to the specified client ID is currently selected and no 564 | * multi-selection exists, or `false` otherwise. 565 | * 566 | * @param state 567 | * @param clientId - Block client ID. 568 | * 569 | * @returns Whether block is selected and multi-selection exists. 570 | */ 571 | export function isBlockSelected( 572 | state: Record< string, unknown >, 573 | clientId: string 574 | ): boolean; 575 | 576 | /** 577 | * Returns whether a block is valid or not. 578 | * 579 | * @param state 580 | * @param clientId - Block client ID. 581 | * 582 | * @returns Is Valid. 583 | */ 584 | export function isBlockValid( 585 | state: Record< string, unknown >, 586 | clientId: string 587 | ): boolean; 588 | 589 | /** 590 | * Returns `true` if the block corresponding to the specified client ID is currently selected but 591 | * isn't the last of the selected blocks. Here "last" refers to the block sequence in the document, 592 | * _not_ the sequence of multi-selection, which is why `state.blockSelection.end` isn't used. 593 | * 594 | * @param state 595 | * @param clientId - Block client ID. 596 | * 597 | * @returns Whether block is selected and not the last in the selection. 598 | */ 599 | export function isBlockWithinSelection( 600 | state: Record< string, unknown >, 601 | clientId: string 602 | ): boolean; 603 | 604 | /** 605 | * Returns `true` if the caret is within formatted text, or `false` otherwise. 606 | * 607 | * @returns Whether the caret is within formatted text. 608 | */ 609 | export function isCaretWithinFormattedText( 610 | state: Record< string, unknown > 611 | ): boolean; 612 | 613 | /** 614 | * Returns `true` if a multi-selection exists, and the block corresponding to the specified client 615 | * ID is the first block of the multi-selection set, or `false` otherwise. 616 | * 617 | * @param state 618 | * @param clientId - Block client ID. 619 | * 620 | * @returns Whether block is first in multi-selection. 621 | */ 622 | export function isFirstMultiSelectedBlock( 623 | state: Record< string, unknown >, 624 | clientId: string 625 | ): boolean; 626 | 627 | /** 628 | * Returns `true` if the most recent block change is be considered persistent, or `false` otherwise. 629 | * A persistent change is one committed by BlockEditorProvider via its `onChange` callback, in 630 | * addition to `onInput`. 631 | * 632 | * @returns Whether the most recent block change was persistent. 633 | */ 634 | export function isLastBlockChangePersistent( 635 | state: Record< string, unknown > 636 | ): boolean; 637 | 638 | /** 639 | * Whether in the process of multi-selecting or not. This flag is only `true` while the 640 | * multi-selection is being selected (by mouse move), and is `false` once the multi-selection has 641 | * been settled. 642 | * 643 | * @see hasMultiSelection 644 | * 645 | * @returns `true` if multi-selecting, `false` if not. 646 | */ 647 | export function isMultiSelecting( state: Record< string, unknown > ): boolean; 648 | 649 | /** 650 | * Selector that returns if multi-selection is enabled or not. 651 | * 652 | * @returns `true` if it should be possible to multi-select blocks, `false` if multi-selection is 653 | * disabled. 654 | */ 655 | export function isSelectionEnabled( state: Record< string, unknown > ): boolean; 656 | 657 | /** 658 | * Returns `true` if the user is typing, or `false` otherwise. 659 | * 660 | * @returns Whether user is typing. 661 | */ 662 | export function isTyping( state: Record< string, unknown > ): boolean; 663 | 664 | /** 665 | * Returns whether the blocks matches the template or not. 666 | * 667 | * @returns Whether the template is valid or not. 668 | */ 669 | export function isValidTemplate( state: Record< string, unknown > ): boolean; 670 | --------------------------------------------------------------------------------