diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000000000000000000000000000000000000..7a7b33aa1dc7083e9536ca09bc4b96fddf7b1f66
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,2 @@
+package-lock.json
+node_modules
diff --git a/README.md b/README.md
index 4d22d1e44e335dfd838a0befa158c9e5a0fde60b..606f2c835ce469ecbeb684efb25b4467eae5ca28 100644
--- a/README.md
+++ b/README.md
@@ -17,3 +17,8 @@ drush en aidmi
     - API Key Input Method: API key in Settings or File Path
         - File Path is recommended. Make sure you save the file outside of web/ (Example on page)
     - API Instructions: Default text provided that is sent to AI for image description.
+
+## Development of CKEditor5 Button
+
+npm install
+npm run build
diff --git a/aidmi.ckeditor5.yml b/aidmi.ckeditor5.yml
new file mode 100644
index 0000000000000000000000000000000000000000..ad0d4974ecc00673b20ac0a9ef9077a1cc0fd5a9
--- /dev/null
+++ b/aidmi.ckeditor5.yml
@@ -0,0 +1,15 @@
+aidmi_aidmi:
+  ckeditor5:
+    plugins:
+      - aidmiPlugin.AIDmi
+  drupal:
+    label: AI, describe my image!
+    toolbar_items:
+      aidmi:
+        label: AI, describe my image!
+    library: aidmi/aidmi_ckeditor
+    elements:
+      - <h2>
+      - <h2 class="aidmi-title">
+      - <div>
+      - <div class="aidmi-description">
diff --git a/aidmi.libraries.yml b/aidmi.libraries.yml
index 3c30b136317a7f68b6efb96312fbc0d635f332d0..2f11e72d2ce1a6f4d645fc3496988a259250332f 100644
--- a/aidmi.libraries.yml
+++ b/aidmi.libraries.yml
@@ -3,8 +3,12 @@ aidmi_ckeditor:
     theme:
       css/aidmi.css: {}
   js:
-    js/aidmi.ckeditor.js: { preprocess: false }
-    js/aidmi.dialog.js: { preprocess: false }
+    js/build/aidmiPlugin.js: { preprocess: false, minified: true }
+    # js/aidmi.ckeditor.js: { preprocess: false }
+    # js/aidmi.dialog.js: { preprocess: false }
   dependencies:
     - core/ckeditor5
+    - core/drupal.dialog.ajax
+    - core/jquery
+    - core/once
     
\ No newline at end of file
diff --git a/aidmi.module b/aidmi.module
index 3c7b5bea32a3ce2a9dc961b049c3d09cbca18dfd..e19114d4d2a5c12a2ec9b9e88ff59f1f1daa00a0 100644
--- a/aidmi.module
+++ b/aidmi.module
@@ -3,11 +3,6 @@
 use Drupal\editor\Entity\Editor;
 use Drupal\ckeditor5\Plugin\CKEditor5PluginDefinition;
 
-/**
- * @file
- * AIDMI module.
- */  
-
 /**
  * Implements hook_help().
  *
@@ -19,11 +14,3 @@ function aidmi_help($route_name, \Drupal\Core\Routing\RouteMatchInterface $route
       return '<p>' . t('The AIDmi module allows users to upload images and get 508-compliant image descriptions generated by AI.') . '</p>';
   }
 }
-
-/**
- * Implements hook_page_attachments() to add JS to CKEditor 5 pages.
- */
-function aidmi_page_attachments(array &$attachments) {
-  // Attach the JS library if CKEditor 5 is active.
-  $attachments['#attached']['library'][] = 'aidmi/aidmi_ckeditor';
-}
diff --git a/aidmi.routing.yml b/aidmi.routing.yml
index 5eb392acd13ff9c520a775491291052000461df4..947661737127b4e344ec3a6ea4868c711b2150c5 100644
--- a/aidmi.routing.yml
+++ b/aidmi.routing.yml
@@ -16,3 +16,13 @@ aidmi.describe_image_ajax:
     fid: \d+
   options:
     _format: 'json'
+
+aidmi.describe_content_ajax:
+  path: '/admin/aidmi/describe-content-ajax'
+  defaults:
+    _controller: 'aidmi.aidmicontroller:analyzeContentAjax'
+    _title: 'AI, describe my image! (AIDmi)'
+  requirements:
+    _permission: 'access content'
+  options:
+    _format: 'json'
\ No newline at end of file
diff --git a/css/aidmi.css b/css/aidmi.css
index d1fbeb1c8fc09975966d2ca869ef77b6ed6ce754..b1ae4ed21a66aec866d5e60cc0d7345ccfb1da48 100644
--- a/css/aidmi.css
+++ b/css/aidmi.css
@@ -19,6 +19,45 @@
     margin-bottom: 10px;
 }
 
+.aidmi-dialog-image {
+    max-width: 100%; 
+    max-height: 400px;
+}
+
+.aidmi-highlight-missing {
+    border: 2px solid red;
+    padding: 10px;
+    margin: 10px 0;
+}
+
 .aidmi-dialog-textarea {
-    resize:vertical;
+    resize: vertical;
+    width: 100%;
+}
+
+.ckeditor5-toolbar-button-aidmi {
+    background-image: url(../icons/aidmi.svg);
+}
+
+/* CKEditor5 AIDmi icon */
+.aidmi-icon-str1 {
+    stroke:#2D2A2B;
+    stroke-width:0.83;
+    stroke-miterlimit:2.61313;
+}
+.aidmi-icon-str0 {
+    stroke:black;
+    stroke-width:0.83;
+    stroke-miterlimit:2.61313;
+}
+.aidmi-icon-fil2 {
+    fill:none;
+}
+.aidmi-icon-fil0 {
+    fill:none;
+    fill-rule:nonzero;
 }
+.aidmi-icon-fil1 {
+    fill:black;
+    fill-rule:nonzero;
+}
\ No newline at end of file
diff --git a/icons/aidmi.svg b/icons/aidmi.svg
new file mode 100644
index 0000000000000000000000000000000000000000..41e7f903393e70952e75088d92865c53626e7321
--- /dev/null
+++ b/icons/aidmi.svg
@@ -0,0 +1,12 @@
+<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" width="20px" height="20px" version="1.1" style="shape-rendering:geometricPrecision; text-rendering:geometricPrecision; image-rendering:optimizeQuality; fill-rule:evenodd; clip-rule:evenodd"
+viewBox="0 0 20 20"
+ xmlns:xlink="http://www.w3.org/1999/xlink">
+ <g id="aidmi-icon-layer">
+  <metadata id="aidmi-icon"/>
+  <path class="aidmi-icon-fil0 aidmi-icon-str0" d="M8.62 17.95c-0.58,0.31 -1.2,0.42 -1.84,0.33 -0.63,-0.08 -1.23,-0.36 -1.7,-0.83l-0.01 -0.01 -0.01 0c-0.75,-0.09 -1.4,-0.45 -1.87,-0.96 -0.46,-0.52 -0.74,-1.18 -0.74,-1.91l0 -0.07 -0.02 -0.01c-0.02,-0.01 -0.04,-0.02 -0.05,-0.03 -0.1,-0.05 -0.17,-0.1 -0.27,-0.17 -0.09,-0.07 -0.18,-0.14 -0.27,-0.21 -0.07,-0.06 -0.13,-0.12 -0.19,-0.18 -0.11,-0.11 -0.21,-0.23 -0.3,-0.35 -0.14,-0.19 -0.26,-0.39 -0.36,-0.6 0,-0.02 -0.01,-0.03 -0.02,-0.03 -0.07,-0.18 -0.13,-0.36 -0.17,-0.55 -0.06,-0.24 -0.09,-0.49 -0.09,-0.73 0,-0.2 0.02,-0.38 0.06,-0.57 0.04,-0.22 0.1,-0.44 0.18,-0.64l0.02 -0.04c0.16,-0.37 0.39,-0.72 0.68,-1.02l0.01 -0.01 0 -0.03c-0.1,-0.4 -0.1,-0.81 -0.02,-1.21 0.03,-0.1 0.06,-0.19 0.09,-0.28 0.03,-0.09 0.05,-0.18 0.09,-0.26 0.15,-0.35 0.37,-0.67 0.66,-0.94l0.01 -0.01 0 -0.03c-0.11,-0.67 0.04,-1.32 0.37,-1.87 0.34,-0.56 0.86,-1 1.52,-1.25l0.02 0 0 -0.02c0.45,-1.12 1.56,-1.86 2.81,-1.86 0.45,0.01 0.88,0.1 1.27,0.29l0 15.62c0.01,0.17 0.05,0.31 0.14,0.44z"/>
+  <path class="aidmi-icon-fil0 aidmi-icon-str0" d="M15.59 16.48c-0.47,0.51 -1.12,0.87 -1.87,0.96l-0.01 0 -0.01 0.01c-0.47,0.47 -1.07,0.75 -1.7,0.83 -0.64,0.09 -1.26,-0.02 -1.84,-0.32 0.09,-0.14 0.14,-0.28 0.14,-0.45l0 -15.62c0.4,-0.19 0.82,-0.28 1.27,-0.29 1.25,0 2.37,0.74 2.81,1.86l0.01 0.02 0.02 0c0.66,0.25 1.18,0.69 1.51,1.25 0.34,0.55 0.48,1.2 0.38,1.87l0 0.03 0.01 0.01c0.28,0.27 0.51,0.59 0.66,0.94 0.04,0.08 0.06,0.17 0.09,0.27 0.03,0.09 0.06,0.18 0.08,0.27 0.09,0.4 0.09,0.81 -0.01,1.21l-0.01 0.03 0.02 0.01c0.29,0.3 0.52,0.65 0.68,1.03l0.01 0.03c0.06,0.14 0.11,0.28 0.14,0.43 -0.68,-0.48 -1.54,-0.85 -2.54,-0.85 -2.77,0 -4.44,2.84 -4.44,2.84l-0.01 0.02 0.01 0.03c0,0 1.67,2.83 4.44,2.83 0.23,0 0.46,-0.02 0.68,-0.06 -0.13,0.3 -0.31,0.57 -0.52,0.81z"/>
+  <path class="aidmi-icon-fil1" d="M20.04 12.85c0,0 -1.73,-2.91 -4.61,-2.91 -2.89,0 -4.62,2.91 -4.62,2.91 0,0 1.73,2.91 4.62,2.91 2.88,0 4.61,-2.91 4.61,-2.91zm-8.55 0c0.28,-0.39 0.6,-0.75 0.96,-1.08 0.74,-0.68 1.75,-1.3 2.98,-1.3 1.22,0 2.24,0.62 2.98,1.3 0.36,0.33 0.68,0.69 0.96,1.08 -0.04,0.05 -0.07,0.1 -0.12,0.15 -0.19,0.26 -0.47,0.59 -0.84,0.93 -0.74,0.68 -1.76,1.3 -2.98,1.3 -1.23,0 -2.24,-0.62 -2.98,-1.3 -0.36,-0.33 -0.68,-0.69 -0.96,-1.08l0 0z"/>
+  <path class="aidmi-icon-fil1" d="M15.63 11.82c-0.88,0 -1.43,0.84 -0.99,1.51 0.21,0.31 0.58,0.5 0.99,0.5 0.89,0 1.44,-0.84 1,-1.51 -0.21,-0.31 -0.59,-0.5 -1,-0.5zm-1.6 1c0,-1.08 1.34,-1.76 2.41,-1.22 0.49,0.25 0.8,0.72 0.8,1.22 0,1.09 -1.34,1.77 -2.41,1.23 -0.5,-0.26 -0.8,-0.72 -0.8,-1.23z"/>
+  <line class="aidmi-icon-fil2 aidmi-icon-str1" x1="12.92" y1="8.92" x2="17.64" y2= "16.79" />
+ </g>
+</svg>
diff --git a/js/build/aidmiPlugin.js b/js/build/aidmiPlugin.js
new file mode 100644
index 0000000000000000000000000000000000000000..25341e962729d28be0ab8156c29f76db44e3d40a
--- /dev/null
+++ b/js/build/aidmiPlugin.js
@@ -0,0 +1 @@
+!function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t():"function"==typeof define&&define.amd?define([],t):"object"==typeof exports?exports.CKEditor5=t():(e.CKEditor5=e.CKEditor5||{},e.CKEditor5.aidmiPlugin=t())}(self,(()=>(()=>{var e={"ckeditor5/src/core.js":(e,t,i)=>{e.exports=i("dll-reference CKEditor5.dll")("./src/core.js")},"ckeditor5/src/ui.js":(e,t,i)=>{e.exports=i("dll-reference CKEditor5.dll")("./src/ui.js")},"dll-reference CKEditor5.dll":e=>{"use strict";e.exports=CKEditor5.dll}},t={};function i(a){var l=t[a];if(void 0!==l)return l.exports;var n=t[a]={exports:{}};return e[a](n,n.exports,i),n.exports}i.d=(e,t)=>{for(var a in t)i.o(t,a)&&!i.o(e,a)&&Object.defineProperty(e,a,{enumerable:!0,get:t[a]})},i.o=(e,t)=>Object.prototype.hasOwnProperty.call(e,t);var a={};return(()=>{"use strict";i.d(a,{default:()=>r});var e=i("ckeditor5/src/core.js"),t=i("ckeditor5/src/ui.js");class l extends e.Plugin{init(){const e=this.editor;e.ui.componentFactory.add("aidmi",(i=>{const a=new t.ButtonView(i);return a.set({label:e.t("AI, describe my image!"),icon:'<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" width="20px" height="20px" version="1.1" style="shape-rendering:geometricPrecision; text-rendering:geometricPrecision; image-rendering:optimizeQuality; fill-rule:evenodd; clip-rule:evenodd"\r\nviewBox="0 0 20 20"\r\n xmlns:xlink="http://www.w3.org/1999/xlink">\r\n <g id="aidmi-icon-layer">\r\n  <metadata id="aidmi-icon"/>\r\n  <path class="aidmi-icon-fil0 aidmi-icon-str0" d="M8.62 17.95c-0.58,0.31 -1.2,0.42 -1.84,0.33 -0.63,-0.08 -1.23,-0.36 -1.7,-0.83l-0.01 -0.01 -0.01 0c-0.75,-0.09 -1.4,-0.45 -1.87,-0.96 -0.46,-0.52 -0.74,-1.18 -0.74,-1.91l0 -0.07 -0.02 -0.01c-0.02,-0.01 -0.04,-0.02 -0.05,-0.03 -0.1,-0.05 -0.17,-0.1 -0.27,-0.17 -0.09,-0.07 -0.18,-0.14 -0.27,-0.21 -0.07,-0.06 -0.13,-0.12 -0.19,-0.18 -0.11,-0.11 -0.21,-0.23 -0.3,-0.35 -0.14,-0.19 -0.26,-0.39 -0.36,-0.6 0,-0.02 -0.01,-0.03 -0.02,-0.03 -0.07,-0.18 -0.13,-0.36 -0.17,-0.55 -0.06,-0.24 -0.09,-0.49 -0.09,-0.73 0,-0.2 0.02,-0.38 0.06,-0.57 0.04,-0.22 0.1,-0.44 0.18,-0.64l0.02 -0.04c0.16,-0.37 0.39,-0.72 0.68,-1.02l0.01 -0.01 0 -0.03c-0.1,-0.4 -0.1,-0.81 -0.02,-1.21 0.03,-0.1 0.06,-0.19 0.09,-0.28 0.03,-0.09 0.05,-0.18 0.09,-0.26 0.15,-0.35 0.37,-0.67 0.66,-0.94l0.01 -0.01 0 -0.03c-0.11,-0.67 0.04,-1.32 0.37,-1.87 0.34,-0.56 0.86,-1 1.52,-1.25l0.02 0 0 -0.02c0.45,-1.12 1.56,-1.86 2.81,-1.86 0.45,0.01 0.88,0.1 1.27,0.29l0 15.62c0.01,0.17 0.05,0.31 0.14,0.44z"/>\r\n  <path class="aidmi-icon-fil0 aidmi-icon-str0" d="M15.59 16.48c-0.47,0.51 -1.12,0.87 -1.87,0.96l-0.01 0 -0.01 0.01c-0.47,0.47 -1.07,0.75 -1.7,0.83 -0.64,0.09 -1.26,-0.02 -1.84,-0.32 0.09,-0.14 0.14,-0.28 0.14,-0.45l0 -15.62c0.4,-0.19 0.82,-0.28 1.27,-0.29 1.25,0 2.37,0.74 2.81,1.86l0.01 0.02 0.02 0c0.66,0.25 1.18,0.69 1.51,1.25 0.34,0.55 0.48,1.2 0.38,1.87l0 0.03 0.01 0.01c0.28,0.27 0.51,0.59 0.66,0.94 0.04,0.08 0.06,0.17 0.09,0.27 0.03,0.09 0.06,0.18 0.08,0.27 0.09,0.4 0.09,0.81 -0.01,1.21l-0.01 0.03 0.02 0.01c0.29,0.3 0.52,0.65 0.68,1.03l0.01 0.03c0.06,0.14 0.11,0.28 0.14,0.43 -0.68,-0.48 -1.54,-0.85 -2.54,-0.85 -2.77,0 -4.44,2.84 -4.44,2.84l-0.01 0.02 0.01 0.03c0,0 1.67,2.83 4.44,2.83 0.23,0 0.46,-0.02 0.68,-0.06 -0.13,0.3 -0.31,0.57 -0.52,0.81z"/>\r\n  <path class="aidmi-icon-fil1" d="M20.04 12.85c0,0 -1.73,-2.91 -4.61,-2.91 -2.89,0 -4.62,2.91 -4.62,2.91 0,0 1.73,2.91 4.62,2.91 2.88,0 4.61,-2.91 4.61,-2.91zm-8.55 0c0.28,-0.39 0.6,-0.75 0.96,-1.08 0.74,-0.68 1.75,-1.3 2.98,-1.3 1.22,0 2.24,0.62 2.98,1.3 0.36,0.33 0.68,0.69 0.96,1.08 -0.04,0.05 -0.07,0.1 -0.12,0.15 -0.19,0.26 -0.47,0.59 -0.84,0.93 -0.74,0.68 -1.76,1.3 -2.98,1.3 -1.23,0 -2.24,-0.62 -2.98,-1.3 -0.36,-0.33 -0.68,-0.69 -0.96,-1.08l0 0z"/>\r\n  <path class="aidmi-icon-fil1" d="M15.63 11.82c-0.88,0 -1.43,0.84 -0.99,1.51 0.21,0.31 0.58,0.5 0.99,0.5 0.89,0 1.44,-0.84 1,-1.51 -0.21,-0.31 -0.59,-0.5 -1,-0.5zm-1.6 1c0,-1.08 1.34,-1.76 2.41,-1.22 0.49,0.25 0.8,0.72 0.8,1.22 0,1.09 -1.34,1.77 -2.41,1.23 -0.5,-0.26 -0.8,-0.72 -0.8,-1.23z"/>\r\n  <line class="aidmi-icon-fil2 aidmi-icon-str1" x1="12.92" y1="8.92" x2="17.64" y2= "16.79" />\r\n </g>\r\n</svg>\r\n',tooltip:!0}),this.listenTo(a,"execute",(async()=>{try{let t=null,i=null,a=this._aidmiJSONImages(e.getData());a.length>0&&(t=await this._aidmiContentAjax(e.getData(),a)),t&&(i=JSON.parse(t),this._openDialog(e,i))}catch(e){console.log("Error:",e)}})),a}))}_aidmiJSONImages(e){const t=document.createElement("div");t.innerHTML=e;let i=t.querySelectorAll("img"),a=[];return i.forEach((e=>{let t={"data-entity-uuid":e.getAttribute("data-entity-uuid"),src:e.getAttribute("src")};a.push(t)})),a}_aidmiContentAjax(e,t){const i=jQuery;return new Promise(((a,l)=>{i.ajax({url:"/admin/aidmi/describe-content-ajax",method:"POST",data:{content:e,imagesJSON:JSON.stringify(t)},success:function(e){a(e)},error:function(e,t,i){l(i)}})}))}_openDialog(e,t,i){const a=document.createElement("div");a.setAttribute("role","dialog"),a.setAttribute("aria-labelledby","aidmi-dialog-title"),a.setAttribute("aria-describedby","aidmi-dialog-description"),a.setAttribute("aria-modal","true");a.innerHTML=`<p id="aidmi-dialog-subtitle" class="aidmi-dialog-subtitle">${Drupal.t("Please evaluate and edit the description as needed.")}</p>`;t.reduce(((e,t)=>(t.forEach((t=>{t.images&&Array.isArray(t.images)&&(e+=t.images.length)})),e)),0);t.forEach((e=>{e.forEach((e=>{e.images&&Array.isArray(e.images)&&e.images.forEach((e=>{let t="";t+=`<div class="image-option-group" id="option-group-${e["data-entity-uuid"]}">`,t+=`\n                            \x3c!-- Image Display --\x3e\n                            <img class="aidmi-dialog-image" data-entity-uuid="${e["data-entity-uuid"]}" src="${e.src}" alt="${Drupal.t(e.before_alt)}" />`;let i=!1,l="Current Alt Tag - Read only",n=e.before_alt;(!n||n.length<6)&&(l+=" (Option disabled since alt is either blank or too short)",i=!0),t+=`\n                            \x3c!-- Current Alt Tag Section --\x3e\n                            <div>\n                                <label for="aidmi-dialog-textarea-oldalt-${e["data-entity-uuid"]}" class="aidmi-dialog-label">\n                                    <input type="radio" name="alt-selection-${e["data-entity-uuid"]}" id="alt-old-${e["data-entity-uuid"]}" value="current" aria-labelledby="label-old-${e["data-entity-uuid"]}" ${i?"disabled":""} required/>\n                                    <span id="label-old-${e["data-entity-uuid"]}">${Drupal.t(l)}</span>\n                                </label>\n                                <textarea id="aidmi-dialog-textarea-oldalt-${e["data-entity-uuid"]}" class="aidmi-dialog-textarea" rows="3" aria-labelledby="label-old-${e["data-entity-uuid"]}" readonly="readonly">${Drupal.t(e.before_alt)}</textarea>\n                            </div>`,t+=`\n                            \x3c!-- Suggested Alt Tag Section --\x3e\n                            <div>\n                                <label for="aidmi-dialog-textarea-newalt-${e["data-entity-uuid"]}" class="aidmi-dialog-label">\n                                    <input type="radio" name="alt-selection-${e["data-entity-uuid"]}" id="alt-new-${e["data-entity-uuid"]}" value="suggested" aria-labelledby="label-new-${e["data-entity-uuid"]}" required>\n                                    <span id="label-new-${e["data-entity-uuid"]}">${Drupal.t("Suggested Alt Tag - Review and modify as required")}</span>\n                                </label>\n                                <textarea id="aidmi-dialog-textarea-newalt-${e["data-entity-uuid"]}" class="aidmi-dialog-textarea" rows="3" aria-labelledby="label-new-${e["data-entity-uuid"]}">${Drupal.t(e.recommendation)}</textarea>\n                            </div>`,t+=`\n                            \x3c!-- Set as Decorative Image --\x3e\n                            <div>\n                                <label for="aidmi-dialog-textarea-dilt-${e["data-entity-uuid"]}" class="aidmi-dialog-label">\n                                    <input type="radio" name="alt-selection-${e["data-entity-uuid"]}" id="alt-decorative-${e["data-entity-uuid"]}" value="decorative" aria-labelledby="label-decorative-${e["data-entity-uuid"]}" required>\n                                    <span id="label-decorative-${e["data-entity-uuid"]}">${Drupal.t("Set as Decorative Image")}</span>\n                                </label>\n                            </div>`,t+="</div><hr/>",a.innerHTML+=t}))}))}));const l={dialogClass:"aidmi-dialog",title:"AIDmi Description",resizable:!0,width:"700px",buttons:[{text:Drupal.t("Insert Text"),class:"aidmi-insert-button",click:()=>{const i=this._collectSelections(t);i?(this._modifyImagesInContent(e,i),n.close()):alert(Drupal.t("Please select an option for each image before inserting."))}},{text:Drupal.t("Cancel"),click:function(){n.close()}}]},n=Drupal.dialog(a,l);n.showModal(),document.querySelectorAll('input[type="radio"]').forEach((e=>{e.addEventListener("change",(e=>{const t=e.target.name.replace("alt-selection-","option-group-");document.getElementById(t).classList.remove("aidmi-highlight-missing")}))})),document.querySelector('input[type="radio"]').focus()}_collectSelections(e){let t=!0;const i=[];return e.forEach((e=>{e.forEach((e=>{e.images&&Array.isArray(e.images)&&e.images.forEach((e=>{const a=e["data-entity-uuid"],l=document.querySelector(`input[name="alt-selection-${a}"]:checked`);if(l){const e=l.value;let t="";"current"===e?t=document.getElementById(`aidmi-dialog-textarea-oldalt-${a}`).value:"suggested"===e&&(t=document.getElementById(`aidmi-dialog-textarea-newalt-${a}`).value),i.push({uuid:a,altText:"decorative"===e?"":t,isDecorative:"decorative"===e})}else t=!1,document.getElementById(`option-group-${a}`).classList.add("aidmi-highlight-missing")}))}))})),t?i:null}_modifyImagesInContent(e,t){let i=e.getData();const a=document.createElement("div");a.innerHTML=i,t.forEach((e=>{const t=a.querySelector(`img[data-entity-uuid="${e.uuid}"]`);t?e.isDecorative?(t.setAttribute("alt",""),t.setAttribute("role","presentation")):(t.setAttribute("alt",e.altText),t.removeAttribute("role")):console.warn(`Image with UUID ${e.uuid} not found in content.`)})),e.setData(a.innerHTML),e.editing.view.document.fire("change:data")}}class n extends e.Plugin{static get requires(){return[l]}static get pluginName(){return"AIDmi"}}const r={AIDmi:n}})(),a=a.default})()));
\ No newline at end of file
diff --git a/js/ckeditor5_plugins/aidmiPlugin/src/aidmi.js b/js/ckeditor5_plugins/aidmiPlugin/src/aidmi.js
new file mode 100644
index 0000000000000000000000000000000000000000..c2476916a7c21da76798f1b019c4e318fbe7a683
--- /dev/null
+++ b/js/ckeditor5_plugins/aidmiPlugin/src/aidmi.js
@@ -0,0 +1,19 @@
+
+import { Plugin } from 'ckeditor5/src/core';
+
+// import AIDmiEditing from './aidmiediting';
+import AIDmiUI from './aidmiui';
+
+export default class AIDmi extends Plugin {
+    static get requires() {
+      // AIDmiEditing, 
+        return [ AIDmiUI ];
+    }
+    /**
+     * @inheritdoc
+     */
+    static get pluginName() {
+      return 'AIDmi';
+    }
+}
+
diff --git a/js/ckeditor5_plugins/aidmiPlugin/src/aidmiui.js b/js/ckeditor5_plugins/aidmiPlugin/src/aidmiui.js
new file mode 100644
index 0000000000000000000000000000000000000000..a32fa34e9b910a4916b487f76f58b01e27b0be09
--- /dev/null
+++ b/js/ckeditor5_plugins/aidmiPlugin/src/aidmiui.js
@@ -0,0 +1,311 @@
+import { Plugin } from 'ckeditor5/src/core';
+import {
+    ButtonView,
+    FocusCycler,
+    LabeledFieldView,
+    SwitchButtonView,
+    View,
+    ViewCollection,
+    createLabeledInputText,
+    injectCssTransitionDisabler,
+    submitHandler,
+} from 'ckeditor5/src/ui';
+
+import iconAidmi from '../../../../icons/aidmi.svg';
+
+export default class AIDmiUI extends Plugin {
+    init() {        
+        const editor = this.editor;
+            
+        // This will register the AIDmi toolbar button.
+        editor.ui.componentFactory.add('aidmi', (locale) => {
+            const button = new ButtonView(locale);
+    
+            // Create the toolbar button.
+            button.set({
+                label: editor.t('AI, describe my image!'),
+                icon: iconAidmi,
+                tooltip: true,
+            });
+        
+            this.listenTo(button, 'execute', async () => {
+                try {
+                    let output = null;
+                    let parsedOutput = null;             
+                    let imagesJSON = this._aidmiJSONImages(editor.getData());
+                    if (imagesJSON.length > 0) {
+                        output = await this._aidmiContentAjax(editor.getData(), imagesJSON);
+                    }                    
+                    if (output) {
+                        parsedOutput = JSON.parse(output);
+                        this._openDialog(editor, parsedOutput);
+                    }
+                } catch (error) {
+                    console.log("Error:", error);
+                }
+            });            
+
+            return button;
+        });
+
+    }
+
+    _aidmiJSONImages(content) {
+        // Create a temporary DOM element to parse the HTML string
+        const tempDiv = document.createElement('div');
+        tempDiv.innerHTML = content; // Convert the HTML string to DOM elements
+        // Find all images with the class 'aidmi-dialog-image'
+        let images = tempDiv.querySelectorAll('img');
+
+        // Initialize an empty array to store the JSON data
+        let imageJsonArray = [];
+
+        // Loop through each image and extract 'data-entity-uuid' and 'src'
+        images.forEach((image) => {
+            let imageObject = {
+                "data-entity-uuid": image.getAttribute('data-entity-uuid'),
+                "src": image.getAttribute('src')
+            };
+            
+            // Push the image object to the array
+            imageJsonArray.push(imageObject);
+        });
+
+        // Convert the array to a JSON string for output or further use
+        return imageJsonArray;
+    }
+
+    _aidmiContentAjax(content, imagesJSON) {   
+        const $ = jQuery;
+        return new Promise((resolve, reject) => {
+            $.ajax({
+                url: '/admin/aidmi/describe-content-ajax',
+                method: 'POST',
+                data: {
+                    content: content,
+                    imagesJSON: JSON.stringify(imagesJSON)
+                },
+                success: function(response) {
+                    resolve(response);
+                },
+                error: function(xhr, status, error) {
+                    reject(error);
+                }
+            });
+        });
+    }
+
+    _openDialog(editor, data, callback) {
+        let aidmiEditedText;
+        // Create a temporary container to hold the content.
+        const tempElement = document.createElement('div');
+        tempElement.setAttribute('role', 'dialog');
+        tempElement.setAttribute('aria-labelledby', 'aidmi-dialog-title');
+        tempElement.setAttribute('aria-describedby', 'aidmi-dialog-description');
+        tempElement.setAttribute('aria-modal', 'true'); // Ensures screen readers treat the dialog as a modal.
+
+        // Set title text.
+        let tempETitle = 'AIDmi Description';
+        let tempESubTitle = 'Please evaluate and edit the description as needed.';
+
+        // Create dialog content with a textarea for editing.
+        tempElement.innerHTML = `<p id="aidmi-dialog-subtitle" class="aidmi-dialog-subtitle">${Drupal.t(tempESubTitle)}</p>`;
+
+        // Variable to store the number of images
+        const imageCount = data.reduce((count, outerArray) => {
+            outerArray.forEach((item) => {
+                if (item.images && Array.isArray(item.images)) {
+                    count += item.images.length;
+                }
+            });
+            return count;
+        }, 0);
+
+        // Loop through the images
+        data.forEach((outerArray) => {
+            outerArray.forEach((item) => {
+                // If item has an images array, loop through it
+                if (item.images && Array.isArray(item.images)) {
+                    item.images.forEach((image) => {
+                        let formImageInnerHTML = '';
+                        // Start of image div alt option area.
+                        formImageInnerHTML += `<div class="image-option-group" id="option-group-${image['data-entity-uuid']}">`;
+                        // Add Image Display.
+                        formImageInnerHTML += `
+                            <!-- Image Display -->
+                            <img class="aidmi-dialog-image" data-entity-uuid="${image['data-entity-uuid']}" src="${image.src}" alt="${Drupal.t(image.before_alt)}" />`;
+                        // Add Current Alt Tag Section.
+                        let imageOldSelectDisabled = false;
+                        let imageLabel = 'Current Alt Tag - Read only';
+                        let imageBeforeAlt = image.before_alt;
+                        if (!imageBeforeAlt || imageBeforeAlt.length < 6) {
+                            // Old alt shouldn't be an allowed option.
+                            imageLabel += ' (Option disabled since alt is either blank or too short)';
+                            imageOldSelectDisabled = true;
+                        }
+                        formImageInnerHTML += `
+                            <!-- Current Alt Tag Section -->
+                            <div>
+                                <label for="aidmi-dialog-textarea-oldalt-${image['data-entity-uuid']}" class="aidmi-dialog-label">
+                                    <input type="radio" name="alt-selection-${image['data-entity-uuid']}" id="alt-old-${image['data-entity-uuid']}" value="current" aria-labelledby="label-old-${image['data-entity-uuid']}" ${imageOldSelectDisabled ? 'disabled' : ''} required/>
+                                    <span id="label-old-${image['data-entity-uuid']}">${Drupal.t(imageLabel)}</span>
+                                </label>
+                                <textarea id="aidmi-dialog-textarea-oldalt-${image['data-entity-uuid']}" class="aidmi-dialog-textarea" rows="3" aria-labelledby="label-old-${image['data-entity-uuid']}" readonly="readonly">${Drupal.t(image.before_alt)}</textarea>
+                            </div>`;   
+                        
+                        // Add Suggested Alt Tag Section.
+                        formImageInnerHTML += `
+                            <!-- Suggested Alt Tag Section -->
+                            <div>
+                                <label for="aidmi-dialog-textarea-newalt-${image['data-entity-uuid']}" class="aidmi-dialog-label">
+                                    <input type="radio" name="alt-selection-${image['data-entity-uuid']}" id="alt-new-${image['data-entity-uuid']}" value="suggested" aria-labelledby="label-new-${image['data-entity-uuid']}" required>
+                                    <span id="label-new-${image['data-entity-uuid']}">${Drupal.t('Suggested Alt Tag - Review and modify as required')}</span>
+                                </label>
+                                <textarea id="aidmi-dialog-textarea-newalt-${image['data-entity-uuid']}" class="aidmi-dialog-textarea" rows="3" aria-labelledby="label-new-${image['data-entity-uuid']}">${Drupal.t(image.recommendation)}</textarea>
+                            </div>`;
+                        // Add Set as Decorative Image.
+                        formImageInnerHTML += `
+                            <!-- Set as Decorative Image -->
+                            <div>
+                                <label for="aidmi-dialog-textarea-dilt-${image['data-entity-uuid']}" class="aidmi-dialog-label">
+                                    <input type="radio" name="alt-selection-${image['data-entity-uuid']}" id="alt-decorative-${image['data-entity-uuid']}" value="decorative" aria-labelledby="label-decorative-${image['data-entity-uuid']}" required>
+                                    <span id="label-decorative-${image['data-entity-uuid']}">${Drupal.t('Set as Decorative Image')}</span>
+                                </label>
+                            </div>`;
+                        // End of image div alt option area.
+                        formImageInnerHTML += `</div><hr/>`;
+                        
+                        // Insert it.
+                        tempElement.innerHTML += formImageInnerHTML;
+                    });
+                }
+            });
+        });
+
+        // Use the Drupal off-canvas dialog to show the content.
+        const options = {
+            dialogClass: 'aidmi-dialog',
+            title: tempETitle,
+            resizable: true,
+            width: '700px',
+            buttons: [
+                {
+                    text: Drupal.t('Insert Text'),
+                    class: 'aidmi-insert-button', // Add a class to the insert button for reference
+                    click: () => {
+                        // Validate all selections before inserting text
+                        const selections = this._collectSelections(data);
+                        if (selections) {
+                            // Modify the images in the editor content based on the selections
+                            this._modifyImagesInContent(editor, selections);
+                            dialogInstance.close();
+                        } else {
+                            // Show a message if validation fails
+                            alert(Drupal.t('Please select an option for each image before inserting.'));
+                        }
+                    }
+                },
+                {
+                    text: Drupal.t('Cancel'),
+                    click: function () {
+                        // Close the dialog.
+                        dialogInstance.close();
+                    }
+                }
+            ]
+        };
+
+        // Open the dialog using Drupal's dialog API.
+        const dialogInstance = Drupal.dialog(tempElement, options);
+        dialogInstance.showModal();
+
+        // Add event listeners to all radio buttons to validate the selection and remove highlights
+        document.querySelectorAll('input[type="radio"]').forEach(radio => {
+            radio.addEventListener('change', (event) => {
+                const groupId = event.target.name.replace('alt-selection-', 'option-group-');
+                // Remove the highlight class when a selection is made
+                document.getElementById(groupId).classList.remove('aidmi-highlight-missing');
+            });
+        });
+
+        // Set focus on the first input for screen readers
+        document.querySelector('input[type="radio"]').focus();
+    }
+
+    _collectSelections(data) {
+        let allSelected = true;
+        const selections = [];
+    
+        // Iterate through each item in the data array to process images
+        data.forEach((outerArray) => {
+            outerArray.forEach((item) => {
+                if (item.images && Array.isArray(item.images)) {
+                    item.images.forEach((image) => {
+                        const uuid = image['data-entity-uuid'];
+                        const selectedRadio = document.querySelector(`input[name="alt-selection-${uuid}"]:checked`);
+                        
+                        // If no radio button is selected, set `allSelected` to false
+                        if (!selectedRadio) {
+                            allSelected = false;
+                            // Highlight the missing selection group
+                            document.getElementById(`option-group-${uuid}`).classList.add('aidmi-highlight-missing');
+                        } else {
+                            // Get the selected value and corresponding alt text or set as decorative
+                            const altValue = selectedRadio.value;
+                            let altText = '';
+                            if (altValue === 'current') {
+                                altText = document.getElementById(`aidmi-dialog-textarea-oldalt-${uuid}`).value;
+                            } else if (altValue === 'suggested') {
+                                altText = document.getElementById(`aidmi-dialog-textarea-newalt-${uuid}`).value;
+                            }
+                            selections.push({
+                                uuid: uuid,
+                                altText: altValue === 'decorative' ? '' : altText, // Leave alt text empty for decorative images
+                                isDecorative: altValue === 'decorative'
+                            });
+                        }
+                    });
+                }
+            });
+        });
+    
+        return allSelected ? selections : null;
+    }
+
+    // Function to modify the images in the CKEditor content based on the selected options
+    _modifyImagesInContent(editor, selections) {
+        // Get the current content of the editor
+        let content = editor.getData();
+
+        // Create a temporary DOM element to manipulate the HTML content
+        const tempDiv = document.createElement('div');
+        tempDiv.innerHTML = content; // Convert the HTML string to DOM elements
+
+        // Loop through the selections and modify the corresponding images in the content
+        selections.forEach(selection => {
+            // Find the image element by its data-entity-uuid attribute
+            const imageElement = tempDiv.querySelector(`img[data-entity-uuid="${selection.uuid}"]`);
+            
+            if (imageElement) {
+                // Update the alt attribute or set as decorative based on the selection
+                if (selection.isDecorative) {
+                    imageElement.setAttribute('alt', ''); // Set alt attribute to empty for decorative images
+                    imageElement.setAttribute('role', 'presentation'); // Add role="presentation" to decorative images
+                } else {
+                    imageElement.setAttribute('alt', selection.altText); // Update the alt attribute
+                    imageElement.removeAttribute('role'); // Remove role attribute if it's not a decorative image
+                }
+                
+            } else {
+                console.warn(`Image with UUID ${selection.uuid} not found in content.`);
+            }
+        });
+
+        // Set the modified content back to the editor
+        editor.setData(tempDiv.innerHTML);
+
+        // Optional: Trigger the change event to update the editor state
+        editor.editing.view.document.fire('change:data');
+    }   
+
+}
diff --git a/js/ckeditor5_plugins/aidmiPlugin/src/index.js b/js/ckeditor5_plugins/aidmiPlugin/src/index.js
new file mode 100644
index 0000000000000000000000000000000000000000..697161c91458e367e4080c0992b6f1f22be42e59
--- /dev/null
+++ b/js/ckeditor5_plugins/aidmiPlugin/src/index.js
@@ -0,0 +1,5 @@
+import AIDmi from './aidmi';
+
+export default {
+  AIDmi: AIDmi,
+};
diff --git a/js/ckeditor5_plugins/aidmiPlugin/src/old.aidmicommand.js b/js/ckeditor5_plugins/aidmiPlugin/src/old.aidmicommand.js
new file mode 100644
index 0000000000000000000000000000000000000000..9aa24d6f74d9f1e234ae57fd7fca5af469bc682a
--- /dev/null
+++ b/js/ckeditor5_plugins/aidmiPlugin/src/old.aidmicommand.js
@@ -0,0 +1,24 @@
+import { Command } from 'ckeditor5/src/core';
+
+
+export default class AIDmiCommand extends Command {
+    execute() {
+        const editor = this.editor;
+        const selection = editor.model.document.selection;
+
+        editor.model.change( writer => {
+            console.write('Hit it!');
+            console.write(editor);
+            console.write(selection);
+        } );
+    }
+
+    refresh() {
+        const model = this.editor.model;
+        const selection = model.document.selection;
+
+        //const isAllowed = model.schema.checkChild( selection.focus.parent, 'aidmiGemini' );
+
+        this.isEnabled = true;
+    }
+}
\ No newline at end of file
diff --git a/js/ckeditor5_plugins/aidmiPlugin/src/old.aidmiediting.js b/js/ckeditor5_plugins/aidmiPlugin/src/old.aidmiediting.js
new file mode 100644
index 0000000000000000000000000000000000000000..4e0a6b0fb4fffd40fa4441596858a61d4ef5bfad
--- /dev/null
+++ b/js/ckeditor5_plugins/aidmiPlugin/src/old.aidmiediting.js
@@ -0,0 +1,34 @@
+import { Plugin } from 'ckeditor5/src/core';
+import { Widget } from 'ckeditor5/src/widget';
+
+import AIDmiCommand from './aidmicommand'; 
+
+export default class AIDmiEditing extends Plugin {
+    //
+    constructor( editor ) {
+        // The default constructor calls the parent constructor.
+        super( editor );
+        // Last fetched value
+        this.externalDataValue = '';
+    }
+
+    static get requires() {
+        return [ Widget ];
+    }
+
+    // This method will help us to clear the interval
+    destroy() {
+        clearInterval( this.intervalId );
+    }
+
+    init() {
+        this.editor.commands.add( 'external', new AIDmiCommand( this.editor ) );
+    }
+
+    /**
+     * @inheritdoc
+     */
+    static get pluginName() {
+      return 'AIDmiEditing';
+    }
+}
diff --git a/package.json b/package.json
new file mode 100644
index 0000000000000000000000000000000000000000..1fb4cb807d25103d091c02408782611279abab44
--- /dev/null
+++ b/package.json
@@ -0,0 +1,22 @@
+{
+  "name": "drupal-ckeditor5",
+  "version": "1.0.0",
+  "description": "Drupal CKEditor 5 integration",
+  "author": "",
+  "license": "GPL-2.0-or-later",
+  "scripts": {
+    "watch": "webpack --mode development --watch",
+    "build": "webpack"
+  },
+  "devDependencies": {
+    "@ckeditor/ckeditor5-dev-utils": "^30.0.0",
+    "ckeditor5": "~34.1.0",
+    "raw-loader": "^4.0.2",
+    "terser-webpack-plugin": "^5.2.0",
+    "webpack": "^5.51.1",
+    "webpack-cli": "^4.4.0"
+  },
+  "dependencies": {
+    "jquery": "^3.7.1"
+  }
+}
diff --git a/src/Controller/AidmiController.php b/src/Controller/AidmiController.php
index 32e9eeb75de7638f7cd00344ef3cc534374a8008..8336de1932bd5ec768a981e1b50de1511e8796bb 100644
--- a/src/Controller/AidmiController.php
+++ b/src/Controller/AidmiController.php
@@ -43,6 +43,19 @@ class AidmiController {
     }
     return null;
   }
+
+  public function analyzeContentAjax(): Response {
+    // Get the raw POST data from the request.
+    $content = \Drupal::request()->request->get('content');
+    $imagesJSON = \Drupal::request()->request->get('imagesJSON');
+    $description = null;
+    
+    if (!empty($content)) {
+      $description = $this->geminiAiService->analyzeContent($content, $imagesJSON);
+    }    
+
+    return new Response($description);
+  }
   
   public function getFileIdByUuid(string $uuid) {
     // Query for the file entity using the UUID.
diff --git a/src/Plugin/CKEditor5Plugin/aidmi.php b/src/Plugin/CKEditor5Plugin/aidmi.php
new file mode 100644
index 0000000000000000000000000000000000000000..96677f1bc1892b347c08a077f359f182f8c5ddd0
--- /dev/null
+++ b/src/Plugin/CKEditor5Plugin/aidmi.php
@@ -0,0 +1,33 @@
+<?php
+
+declare(strict_types = 1);
+
+namespace Drupal\aidmi\Plugin\CKEditor5Plugin;
+
+use Drupal\ckeditor5\Plugin\CKEditor5PluginDefault;
+use Drupal\Core\Url;
+use Drupal\editor\EditorInterface;
+
+/**
+ * Plugin class to add dialog url for embedded content.
+ */
+class aidmi extends CKEditor5PluginDefault {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getDynamicPluginConfig(array $static_plugin_config, EditorInterface $editor): array {
+    $aidmi_dialog_url = Url::fromRoute('ckeditor5_aidmi.dialog')
+      ->toString(TRUE)
+      ->getGeneratedUrl();
+    $static_plugin_config['aidmi']['dialogURL'] = $aidmi_dialog_url;
+    $aidmi_preview_url = Url::fromRoute('ckeditor5_aidmi.preview', [
+      'editor' => $editor->id(),
+    ])
+      ->toString(TRUE)
+      ->getGeneratedUrl();
+    // $static_plugin_config['aidmi']['previewURL'] = $aidmi_preview_url;
+    return $static_plugin_config;
+  }
+
+}
diff --git a/src/Service/GeminiAiService.php b/src/Service/GeminiAiService.php
index f6fc68d0f090b1d246252dd5de23d9c1a6ff0f1a..d7ee8c9df44689869bbfcc95072040f30ab6b4af 100644
--- a/src/Service/GeminiAiService.php
+++ b/src/Service/GeminiAiService.php
@@ -3,6 +3,8 @@
 namespace Drupal\aidmi\Service;
 
 use GeminiAPI\Client;
+use GeminiAPI\GenerationConfig;
+use GeminiAPI\Resources\Parts\PartInterface;
 use GeminiAPI\Enums\MimeType;
 use GeminiAPI\Resources\Parts\ImagePart;
 use GeminiAPI\Resources\Parts\TextPart;
@@ -33,9 +35,16 @@ class GeminiAiService {
    */
   protected $apiInstructions;
 
+  /**
+   * The AIDmi controller.
+   *
+   * @var \Drupal\aidmi\Controller\AidmiController
+   */
+  protected $aidmiController;
+  
+
   public function __construct(ConfigFactoryInterface $configFactory) {
     $this->configFactory = $configFactory;
-    
     // Retrieve the configuration for the module.
     $config = $configFactory->get('aidmi.settings');
     // Get the input method.
@@ -68,7 +77,7 @@ class GeminiAiService {
         $apiKey = '0';
       }
     }
-    
+        
     $this->client = new Client($apiKey);
     $this->apiInstructions = $config->get('api_instructions');
   }
@@ -102,4 +111,59 @@ class GeminiAiService {
     }
   }
   
+  public function analyzeContent(string $content, string $imagesJSON): string {
+    try {
+      // Decode JSON string into PHP array
+      $jsonArray = json_decode($imagesJSON, true);
+            
+      $generateContent[] = new TextPart('Response can only be pure JSON, do not provide null value if something is empty, just keep it "" blank. Example: [{"images": [{"data-entity-uuid": "", "src": "", "before_alt": "", "recommendation": "", "decorative_image" = ""}]}, {"message": "error or other message if needed"}]. Review this content and describe the image. Actual images from the content are attached and referenced by data-entity-uuid.  Try to provide some text in context for an accessibilty alt text. For graphs, make sure the description provides a detail for the reader to understand and include some relevant information perceived from it. For individuals, provide a short description. Try to provide short details based on context of where image is used in text. Provide a json output with each image by data-entity-uuid, image src, the before alt text if available, and your recommendation. If there is a problem, respond with only an error JSON. Provide a true or false for decorative image where just an alt tag alone is decorative, but alt="" is false.');
+      $generateContent[] = new TextPart($content);
+
+      // Loop through the array of files in content.
+      foreach ($jsonArray as $item) {
+        // Get file and contents.
+        $file = $this->getFileIdByUuid($item['data-entity-uuid']);
+        $imagePath = $file->getFileUri();
+        $imageContent = file_get_contents($imagePath);
+        if (!$imageContent) {
+          throw new \Exception($this->t('Unable to open image file.'));
+        }
+        
+        // Attach images for Gemini AI review.
+        $generateContent[] = new TextPart("data-entity-uuid: ". $item['data-entity-uuid']);
+        $generateContent[] = new ImagePart(
+              MimeType::IMAGE_JPEG,
+              base64_encode($imageContent)
+        );
+      }
+
+      // Create the response.
+      $response = $this->client
+        ->withRequestHeaders([
+          'response_mime_type' => 'application/json'
+        ])->geminiProFlash1_5()
+        ->generateContent(...$generateContent);
+
+      // Convert to full JSON and return the description from Gemini API.
+      $output = $response->text();
+      $output = str_replace("```json","[", $output);
+      $output = str_replace("```","]", $output);
+
+      return $output;
+    } catch (\Exception $e) {
+        return 'Error: ' . $e->getMessage();
+    }
+  }
+  
+  public function getFileIdByUuid(string $uuid) {
+    // Query for the file entity using the UUID.
+    $file = \Drupal::entityTypeManager()->getStorage('file')->loadByProperties(['uuid' => $uuid]);
+
+    if ($file) {
+      $file = reset($file);  // Get the first result.
+      return $file;
+    } else {
+      throw new \Exception($this->t('File not found.'));
+    }
+  }
 }
diff --git a/webpack.config.js b/webpack.config.js
new file mode 100644
index 0000000000000000000000000000000000000000..a4c073ecd99170c329ef577322f162673e6fd166
--- /dev/null
+++ b/webpack.config.js
@@ -0,0 +1,66 @@
+const path = require('path');
+const fs = require('fs');
+const webpack = require('webpack');
+const { styles, builds } = require('@ckeditor/ckeditor5-dev-utils');
+const TerserPlugin = require('terser-webpack-plugin');
+
+function getDirectories(srcpath) {
+  return fs
+    .readdirSync(srcpath)
+    .filter((item) => fs.statSync(path.join(srcpath, item)).isDirectory());
+}
+
+module.exports = [];
+// Loop through every subdirectory in src, each a different plugin, and build
+// each one in ./build.
+getDirectories('./js/ckeditor5_plugins').forEach((dir) => {
+  const bc = {
+    mode: 'production',
+    optimization: {
+      minimize: true,
+      minimizer: [
+        new TerserPlugin({
+          terserOptions: {
+            format: {
+              comments: false,
+            },
+          },
+          test: /\.js(\?.*)?$/i,
+          extractComments: false,
+        }),
+      ],
+      moduleIds: 'named',
+    },
+    entry: {
+      path: path.resolve(
+        __dirname,
+        'js/ckeditor5_plugins',
+        dir,
+        'src/index.js',
+      ),
+    },
+    output: {
+      path: path.resolve(__dirname, './js/build'),
+      filename: `${dir}.js`,
+      library: ['CKEditor5', dir],
+      libraryTarget: 'umd',
+      libraryExport: 'default',
+    },
+    plugins: [
+      // It is possible to require the ckeditor5-dll.manifest.json used in
+      // core/node_modules rather than having to install CKEditor 5 here.
+      // However, that requires knowing the location of that file relative to
+      // where your module code is located.
+      new webpack.DllReferencePlugin({
+        manifest: require('./node_modules/ckeditor5/build/ckeditor5-dll.manifest.json'), // eslint-disable-line global-require, import/no-unresolved
+        scope: 'ckeditor5/src',
+        name: 'CKEditor5.dll',
+      }),
+    ],
+    module: {
+      rules: [{ test: /\.svg$/, use: 'raw-loader' }],
+    },
+  };
+
+  module.exports.push(bc);
+});