From 99d73dd56a32d0af59c6c65c5c66cae3e56d8c15 Mon Sep 17 00:00:00 2001 From: Linnea Huxford <7308162+mslinnea@users.noreply.github.com> Date: Sat, 16 Nov 2024 07:50:58 -0800 Subject: [PATCH 1/4] adds alt text icon --- package-lock.json | 2 ++ package.json | 2 ++ src/components/altText/index.js | 0 src/components/blockControls/index.js | 21 +++++++++++++++++++ src/components/imageControls/index.js | 19 +++++++++++++++++ .../index.js | 0 src/index.js | 3 ++- 7 files changed, 46 insertions(+), 1 deletion(-) create mode 100644 src/components/altText/index.js create mode 100644 src/components/blockControls/index.js create mode 100644 src/components/imageControls/index.js rename src/components/{settingsScreen => settingsPanel}/index.js (100%) diff --git a/package-lock.json b/package-lock.json index 2cd4ad9..8983925 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,8 @@ "@wordpress/components": "^28.0.3", "@wordpress/data": "^10.0.2", "@wordpress/editor": "^14.0.8", + "@wordpress/hooks": "^4.11.0", + "@wordpress/i18n": "^5.11.0", "@wordpress/plugins": "^7.0.3", "mustache": "^4.2.0", "react": "^18.3.1", diff --git a/package.json b/package.json index c8151fb..da8d4a8 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,8 @@ "@wordpress/components": "^28.0.3", "@wordpress/data": "^10.0.2", "@wordpress/editor": "^14.0.8", + "@wordpress/hooks": "^4.11.0", + "@wordpress/i18n": "^5.11.0", "@wordpress/plugins": "^7.0.3", "mustache": "^4.2.0", "react": "^18.3.1", diff --git a/src/components/altText/index.js b/src/components/altText/index.js new file mode 100644 index 0000000..e69de29 diff --git a/src/components/blockControls/index.js b/src/components/blockControls/index.js new file mode 100644 index 0000000..1df1159 --- /dev/null +++ b/src/components/blockControls/index.js @@ -0,0 +1,21 @@ +import { addFilter } from '@wordpress/hooks'; +import ImageControls from '../imageControls'; + +const addToolbar = (BlockEdit) => (props) => { + const { name } = props; + if (name === 'core/image') { + return ( + <> + + + + ); + } + return ; +}; + +addFilter( + 'editor.BlockEdit', + 'ai-seo-tools/add-block-toolbar', + addToolbar, +); diff --git a/src/components/imageControls/index.js b/src/components/imageControls/index.js new file mode 100644 index 0000000..cbf1a92 --- /dev/null +++ b/src/components/imageControls/index.js @@ -0,0 +1,19 @@ +import { __ } from '@wordpress/i18n'; +import { Icon, ToolbarButton } from '@wordpress/components'; +import { useState } from 'react'; +import { BlockControls } from '@wordpress/block-editor'; + +export default function ImageControls() { + const [inProgress, setInProgress] = useState(false); + return ( + + console.log('alt text clicked')} + /> + + ); +} diff --git a/src/components/settingsScreen/index.js b/src/components/settingsPanel/index.js similarity index 100% rename from src/components/settingsScreen/index.js rename to src/components/settingsPanel/index.js diff --git a/src/index.js b/src/index.js index 521a2a6..59c27f8 100644 --- a/src/index.js +++ b/src/index.js @@ -1 +1,2 @@ -import './components/settingsScreen'; +import './components/settingsPanel'; +import './components/blockControls'; From e3f6be2ba1c64b5ac3c3b350f8751826329a026a Mon Sep 17 00:00:00 2001 From: Linnea Huxford <7308162+mslinnea@users.noreply.github.com> Date: Sat, 16 Nov 2024 08:55:14 -0800 Subject: [PATCH 2/4] integrate AI for alt text --- src/components/altText/index.js | 66 +++++++++++++++++++++++++++ src/components/blockControls/index.js | 4 +- src/components/imageControls/index.js | 20 +++++++- src/helpers/image-helpers.js | 30 ++++++++++++ 4 files changed, 116 insertions(+), 4 deletions(-) create mode 100644 src/helpers/image-helpers.js diff --git a/src/components/altText/index.js b/src/components/altText/index.js index e69de29..d3cc771 100644 --- a/src/components/altText/index.js +++ b/src/components/altText/index.js @@ -0,0 +1,66 @@ +import { __ } from '@wordpress/i18n'; +import { getMimeType, getBase64Image } from '../../helpers/image-helpers'; + +const { enums, helpers, store: aiStore } = window.aiServices.ai; +const AI_CAPABILITIES = [enums.AiCapability.MULTIMODAL_INPUT, enums.AiCapability.TEXT_GENERATION]; + +export default async function generateAltText(props, setInProgress, service) { + const { attributes, setAttributes } = props; + + // Check if AI service is available + if (!service) { + console.error('AI service is not available.'); + return; + } + + // Check if image URL is available + if (!attributes.url) { + console.error('Image URL is missing.'); + return; + } + + setInProgress(true); + + // Fetch the image data and prepare the request + try { + const mimeType = getMimeType(attributes.url); + const base64Image = await getBase64Image(attributes.url); + + // Call the AI service to generate alt text + const candidates = await service.generateText( + { + role: enums.ContentRole.USER, + parts: [ + { + text: __( + 'Create a brief description of what the following image shows, suitable as alternative text for screen readers.', + 'ai-seo-tools', + ), + }, + { + inlineData: { + mimeType, + data: base64Image, + }, + }, + ], + }, + { + feature: 'ai-seo-tools-alt-text', + capabilities: AI_CAPABILITIES, + }, + ); + + // Extract the generated alt text + const alt = helpers + .getTextFromContents(helpers.getCandidateContents(candidates)) + .replaceAll('\n\n\n\n', '\n\n'); + + // Set the alt text attribute + setAttributes({ alt }); + } catch (error) { + console.error('Error generating alt text:', error); + } finally { + setInProgress(false); + } +} diff --git a/src/components/blockControls/index.js b/src/components/blockControls/index.js index 1df1159..18c9acf 100644 --- a/src/components/blockControls/index.js +++ b/src/components/blockControls/index.js @@ -1,7 +1,7 @@ import { addFilter } from '@wordpress/hooks'; import ImageControls from '../imageControls'; -const addToolbar = (BlockEdit) => (props) => { +const addToolbar = (BlockEdit) => function(props) { const { name } = props; if (name === 'core/image') { return ( @@ -11,7 +11,7 @@ const addToolbar = (BlockEdit) => (props) => { ); } - return ; + return ; }; addFilter( diff --git a/src/components/imageControls/index.js b/src/components/imageControls/index.js index cbf1a92..ce16dec 100644 --- a/src/components/imageControls/index.js +++ b/src/components/imageControls/index.js @@ -2,9 +2,25 @@ import { __ } from '@wordpress/i18n'; import { Icon, ToolbarButton } from '@wordpress/components'; import { useState } from 'react'; import { BlockControls } from '@wordpress/block-editor'; +import generateAltText from '../altText/index'; +import {useSelect} from "@wordpress/data"; -export default function ImageControls() { +const { enums, helpers, store: aiStore } = window.aiServices.ai; +const AI_CAPABILITIES = [enums.AiCapability.MULTIMODAL_INPUT, enums.AiCapability.TEXT_GENERATION]; + +export default function ImageControls(props) { const [inProgress, setInProgress] = useState(false); + const service = useSelect((select) => select(aiStore) + .getAvailableService( + { capabilities: AI_CAPABILITIES }, + )); + + const handleClick = async () => { + setInProgress(true); + await generateAltText(props, setInProgress, service); + setInProgress(false); + }; + return ( console.log('alt text clicked')} + onClick={handleClick} /> ); diff --git a/src/helpers/image-helpers.js b/src/helpers/image-helpers.js new file mode 100644 index 0000000..41a5fb3 --- /dev/null +++ b/src/helpers/image-helpers.js @@ -0,0 +1,30 @@ +export function getMimeType(url) { + const extension = url.split('.').pop().toLowerCase(); + switch (extension) { + case 'png': + return 'image/png'; + case 'gif': + return 'image/gif'; + case 'avif': + return 'image/avif'; + case 'webp': + return 'image/webp'; + case 'jpg': + case 'jpeg': + default: + return 'image/jpeg'; + } +} + +export async function getBase64Image(url) { + const data = await fetch(url); + const blob = await data.blob(); + return new Promise((resolve) => { + const reader = new FileReader(); + reader.readAsDataURL(blob); + reader.onloadend = () => { + const base64data = reader.result; + resolve(base64data); + }; + }); +} From b731b28f68fd9379441f07016ffa3471f26cccc1 Mon Sep 17 00:00:00 2001 From: Linnea Huxford <7308162+mslinnea@users.noreply.github.com> Date: Sat, 16 Nov 2024 20:50:48 -0800 Subject: [PATCH 3/4] add image alt text feature --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 28b6b81..bcecc42 100644 --- a/README.md +++ b/README.md @@ -7,6 +7,7 @@ be compatible with all AI services supported by the [AI Services Plugin](https:/ - Generate Meta Description - Generate Meta Keywords +- Generate Image Alt Text via Image Block toolbar button # Demo with OpenAI gpt-3.5-turbo model From bc5fa05b2880e7622fda400750b7e1b2925c1016 Mon Sep 17 00:00:00 2001 From: Linnea Huxford <7308162+mslinnea@users.noreply.github.com> Date: Sat, 16 Nov 2024 20:57:51 -0800 Subject: [PATCH 4/4] lint --- package-lock.json | 8 +++++--- package.json | 1 + src/components/altText/index.js | 5 ++++- src/components/blockControls/index.js | 16 +++++++++------- src/components/imageControls/index.js | 10 +++++----- 5 files changed, 24 insertions(+), 16 deletions(-) diff --git a/package-lock.json b/package-lock.json index 8983925..ff97952 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7,6 +7,7 @@ "dependencies": { "@alleyinteractive/block-editor-tools": "^0.5.1", "@wordpress/api-fetch": "^7.0.1", + "@wordpress/block-editor": "^14.7.0", "@wordpress/components": "^28.0.3", "@wordpress/data": "^10.0.2", "@wordpress/editor": "^14.0.8", @@ -5819,9 +5820,9 @@ } }, "node_modules/@wordpress/block-editor": { - "version": "14.6.0", - "resolved": "https://registry.npmjs.org/@wordpress/block-editor/-/block-editor-14.6.0.tgz", - "integrity": "sha512-Pala/+i7izwB7QJbr6tVfg2XR0p6wb2biIoF2DjzNh+draoTjPtxCbZQrOJSd8hfyXQOOQPuLiAGdsq50PEZsA==", + "version": "14.7.0", + "resolved": "https://registry.npmjs.org/@wordpress/block-editor/-/block-editor-14.7.0.tgz", + "integrity": "sha512-19PJE3rt0xTPhP/clFuL2qwJ00O03xyvw2/Jg1bFFvaLydm27KalO/7aiMJSDpOklT5w5xjWYwBKw6yl6IYJIA==", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "7.25.7", @@ -5851,6 +5852,7 @@ "@wordpress/keycodes": "*", "@wordpress/notices": "*", "@wordpress/preferences": "*", + "@wordpress/priority-queue": "*", "@wordpress/private-apis": "*", "@wordpress/rich-text": "*", "@wordpress/style-engine": "*", diff --git a/package.json b/package.json index da8d4a8..6be0e80 100644 --- a/package.json +++ b/package.json @@ -14,6 +14,7 @@ "dependencies": { "@alleyinteractive/block-editor-tools": "^0.5.1", "@wordpress/api-fetch": "^7.0.1", + "@wordpress/block-editor": "^14.7.0", "@wordpress/components": "^28.0.3", "@wordpress/data": "^10.0.2", "@wordpress/editor": "^14.0.8", diff --git a/src/components/altText/index.js b/src/components/altText/index.js index d3cc771..44c59b6 100644 --- a/src/components/altText/index.js +++ b/src/components/altText/index.js @@ -1,7 +1,7 @@ import { __ } from '@wordpress/i18n'; import { getMimeType, getBase64Image } from '../../helpers/image-helpers'; -const { enums, helpers, store: aiStore } = window.aiServices.ai; +const { enums, helpers } = window.aiServices.ai; const AI_CAPABILITIES = [enums.AiCapability.MULTIMODAL_INPUT, enums.AiCapability.TEXT_GENERATION]; export default async function generateAltText(props, setInProgress, service) { @@ -9,12 +9,14 @@ export default async function generateAltText(props, setInProgress, service) { // Check if AI service is available if (!service) { + // eslint-disable-next-line no-console console.error('AI service is not available.'); return; } // Check if image URL is available if (!attributes.url) { + // eslint-disable-next-line no-console console.error('Image URL is missing.'); return; } @@ -59,6 +61,7 @@ export default async function generateAltText(props, setInProgress, service) { // Set the alt text attribute setAttributes({ alt }); } catch (error) { + // eslint-disable-next-line no-console console.error('Error generating alt text:', error); } finally { setInProgress(false); diff --git a/src/components/blockControls/index.js b/src/components/blockControls/index.js index 18c9acf..045584a 100644 --- a/src/components/blockControls/index.js +++ b/src/components/blockControls/index.js @@ -1,15 +1,17 @@ import { addFilter } from '@wordpress/hooks'; import ImageControls from '../imageControls'; -const addToolbar = (BlockEdit) => function(props) { - const { name } = props; +// eslint-disable-next-line func-names +const addToolbar = (BlockEdit) => function (props) { + // eslint-disable-next-line react/prop-types + const { name } = props; if (name === 'core/image') { return ( - <> - - - - ); + <> + + + + ); } return ; }; diff --git a/src/components/imageControls/index.js b/src/components/imageControls/index.js index ce16dec..886dcbc 100644 --- a/src/components/imageControls/index.js +++ b/src/components/imageControls/index.js @@ -1,19 +1,19 @@ import { __ } from '@wordpress/i18n'; -import { Icon, ToolbarButton } from '@wordpress/components'; +import { ToolbarButton } from '@wordpress/components'; import { useState } from 'react'; import { BlockControls } from '@wordpress/block-editor'; +import { useSelect } from '@wordpress/data'; import generateAltText from '../altText/index'; -import {useSelect} from "@wordpress/data"; -const { enums, helpers, store: aiStore } = window.aiServices.ai; +const { enums, store: aiStore } = window.aiServices.ai; const AI_CAPABILITIES = [enums.AiCapability.MULTIMODAL_INPUT, enums.AiCapability.TEXT_GENERATION]; export default function ImageControls(props) { const [inProgress, setInProgress] = useState(false); const service = useSelect((select) => select(aiStore) - .getAvailableService( + .getAvailableService( { capabilities: AI_CAPABILITIES }, - )); + )); const handleClick = async () => { setInProgress(true);