Create a generic AI plugin layer for CKEditor
>>> [!note] Migrated issue
<!-- Drupal.org comment -->
<!-- Migrated from issue #3462674. -->
Reported by: [kevinquillen](https://www.drupal.org/user/317279)
Related to !28
>>>
<h3 id="summary-problem-motivation">Problem/Motivation</h3>
<p>The CKEditor code from OpenAI worked to an extent but falls short on maintenance and extendability. Part of what I was waiting on was to see if the CKEditor API introduced better UI layers for enhancing what you see, better dialogs, better ways of selecting options, dialog handling, etc. As it stands, the CKEditor UI API seems too complex for fast changes.</p>
<p>What we should have is a generic plugin layer that lets you register editor functionality via PHP plugins, and their actions open in Drupal dialogs and returned via AJAX. This would reduce the code needed for pure CKEditor JS and move it back to Drupal which is more native for this audience.</p>
<p>That way, we can provide the current actions as PHP plugins and the community could extend it further. Also, we could get the ability to do 'retry' or 'regenerate' responses if the initial answer for an action is not satisfactory.</p>
<h3 id="summary-proposed-resolution">Proposed resolution</h3>
<p>Implement a plugin layer that can be used. The default plugin should pre configure and handle Drupal dialog handling so any implemented plugins just need to provide the form and submission action.</p>
<p>Here is partial WIP code of what a plugin form would do.</p>
<p>When selecting an action in the CKEditor dropdown in the editor, the JS fires a callback to a Drupal route (provided by the plugin layer somehow). The response is a Drupal form within the dialog. </p>
<p>This is the Tone action converted to a FormBase form:</p>
<div class="codeblock">
<pre><span style="color: #000000"><span style="color: #0000BB"><?php<br> </span><span style="color: #007700">public function </span><span style="color: #0000BB">buildForm</span><span style="color: #007700">(array </span><span style="color: #0000BB">$form</span><span style="color: #007700">, </span><span style="color: #0000BB">FormStateInterface $form_state</span><span style="color: #007700">) {<br><br> </span><span style="color: #0000BB">$form</span><span style="color: #007700">[</span><span style="color: #DD0000">'tone'</span><span style="color: #007700">] = [<br> </span><span style="color: #DD0000">'#type' </span><span style="color: #007700">=> </span><span style="color: #DD0000">'select'</span><span style="color: #007700">,<br> </span><span style="color: #DD0000">'#title' </span><span style="color: #007700">=> </span><span style="color: #0000BB">t</span><span style="color: #007700">(</span><span style="color: #DD0000">'Choose tone'</span><span style="color: #007700">),<br> </span><span style="color: #DD0000">'#description' </span><span style="color: #007700">=> </span><span style="color: #0000BB">t</span><span style="color: #007700">(</span><span style="color: #DD0000">'Selecting one of the options will adjust/reword the body content to be appropriate for the target audience.'</span><span style="color: #007700">),<br> </span><span style="color: #DD0000">'#options' </span><span style="color: #007700">=> [<br> </span><span style="color: #DD0000">'friendly' </span><span style="color: #007700">=> </span><span style="color: #0000BB">t</span><span style="color: #007700">(</span><span style="color: #DD0000">'Friendly'</span><span style="color: #007700">),<br> </span><span style="color: #DD0000">'professional' </span><span style="color: #007700">=> </span><span style="color: #0000BB">t</span><span style="color: #007700">(</span><span style="color: #DD0000">'Professional'</span><span style="color: #007700">),<br> </span><span style="color: #DD0000">'helpful' </span><span style="color: #007700">=> </span><span style="color: #0000BB">t</span><span style="color: #007700">(</span><span style="color: #DD0000">'Helpful'</span><span style="color: #007700">),<br> </span><span style="color: #DD0000">'easier for a high school educated reader' </span><span style="color: #007700">=> </span><span style="color: #0000BB">t</span><span style="color: #007700">(</span><span style="color: #DD0000">'High school level reader'</span><span style="color: #007700">),<br> </span><span style="color: #DD0000">'easier for a college educated reader' </span><span style="color: #007700">=> </span><span style="color: #0000BB">t</span><span style="color: #007700">(</span><span style="color: #DD0000">'College level reader'</span><span style="color: #007700">),<br> </span><span style="color: #DD0000">'explained to a five year old' </span><span style="color: #007700">=> </span><span style="color: #0000BB">t</span><span style="color: #007700">(</span><span style="color: #DD0000">'Explain like I\'m 5'</span><span style="color: #007700">),<br> ],<br> ];<br><br> </span><span style="color: #0000BB">$form</span><span style="color: #007700">[</span><span style="color: #DD0000">'actions'</span><span style="color: #007700">] = [<br> </span><span style="color: #DD0000">'#type' </span><span style="color: #007700">=> </span><span style="color: #DD0000">'actions'</span><span style="color: #007700">,<br> ];<br><br> </span><span style="color: #0000BB">$form</span><span style="color: #007700">[</span><span style="color: #DD0000">'actions'</span><span style="color: #007700">][</span><span style="color: #DD0000">'submit'</span><span style="color: #007700">] = [<br> </span><span style="color: #DD0000">'#type' </span><span style="color: #007700">=> </span><span style="color: #DD0000">'button'</span><span style="color: #007700">,<br> </span><span style="color: #DD0000">'#value' </span><span style="color: #007700">=> </span><span style="color: #0000BB">$this</span><span style="color: #007700">-></span><span style="color: #0000BB">t</span><span style="color: #007700">(</span><span style="color: #DD0000">'Submit'</span><span style="color: #007700">),<br> </span><span style="color: #DD0000">'#ajax' </span><span style="color: #007700">=> [<br> </span><span style="color: #DD0000">'callback' </span><span style="color: #007700">=> [</span><span style="color: #0000BB">$this</span><span style="color: #007700">, </span><span style="color: #DD0000">'ajaxSubmitForm'</span><span style="color: #007700">],<br> ],<br> ];<br><br> return </span><span style="color: #0000BB">$form</span><span style="color: #007700">;<br> }<br><br> </span><span style="color: #FF8000">/**<br> * {@inheritdoc}<br> */<br> </span><span style="color: #007700">public function </span><span style="color: #0000BB">validateForm</span><span style="color: #007700">(array &</span><span style="color: #0000BB">$form</span><span style="color: #007700">, </span><span style="color: #0000BB">FormStateInterface $form_state</span><span style="color: #007700">) {<br><br> }<br><br> </span><span style="color: #FF8000">/**<br> * {@inheritdoc}<br> */<br> </span><span style="color: #007700">public function </span><span style="color: #0000BB">ajaxSubmitForm</span><span style="color: #007700">(array &</span><span style="color: #0000BB">$form</span><span style="color: #007700">, </span><span style="color: #0000BB">FormStateInterface $form_state</span><span style="color: #007700">) {<br> </span><span style="color: #0000BB">$response </span><span style="color: #007700">= new </span><span style="color: #0000BB">AjaxResponse</span><span style="color: #007700">();<br> </span><span style="color: #0000BB">$response</span><span style="color: #007700">-></span><span style="color: #0000BB">addCommand</span><span style="color: #007700">(new </span><span style="color: #0000BB">EditorDialogSave</span><span style="color: #007700">([<br> </span><span style="color: #DD0000">'attributes' </span><span style="color: #007700">=> [<br> </span><span style="color: #DD0000">'prompt' </span><span style="color: #007700">=> </span><span style="color: #DD0000">'Change the tone of the following text to be more ' </span><span style="color: #007700">. </span><span style="color: #0000BB">$form_state</span><span style="color: #007700">-></span><span style="color: #0000BB">getValue</span><span style="color: #007700">([</span><span style="color: #DD0000">'tone'</span><span style="color: #007700">]) . </span><span style="color: #DD0000">' using the same language as the following text: '</span><span style="color: #007700">,<br> </span><span style="color: #DD0000">'endpoint' </span><span style="color: #007700">=> </span><span style="color: #DD0000">'api/openai-ckeditor/completion'</span><span style="color: #007700">,<br> </span><span style="color: #DD0000">'options' </span><span style="color: #007700">=> [<br> </span><span style="color: #DD0000">'model' </span><span style="color: #007700">=> </span><span style="color: #DD0000">'gpt-3.5-turbo'</span><span style="color: #007700">,<br> </span><span style="color: #DD0000">'max_tokens' </span><span style="color: #007700">=> </span><span style="color: #0000BB">2048</span><span style="color: #007700">,<br> </span><span style="color: #DD0000">'temperature' </span><span style="color: #007700">=> </span><span style="color: #0000BB">0.4</span><span style="color: #007700">,<br> ]<br> ],<br> ]));<br> </span><span style="color: #0000BB">$response</span><span style="color: #007700">-></span><span style="color: #0000BB">addCommand</span><span style="color: #007700">(new </span><span style="color: #0000BB">CloseModalDialogCommand</span><span style="color: #007700">());<br> return </span><span style="color: #0000BB">$response</span><span style="color: #007700">;<br> }<br></span><span style="color: #0000BB">?></span></span></pre></div>
<p><code>$response->addCommand(new EditorDialogSave</code> (<a href="https://api.drupal.org/api/drupal/core%21modules%21editor%21src%21Ajax%21EditorDialogSave.php/11.x">https://api.drupal.org/api/drupal/core%21modules%21editor%21src%21Ajax%21EditorDialogSave.php/11.x</a>) is the key part that puts the response directly back into the editor. </p>
<pre> <br>/**<br> * Command to save the contents of an editor-provided modal.<br> *<br> * This command does not close the open modal. It should be followed by a<br> * call to `Drupal.AjaxCommands.prototype.closeDialog`. Editors that are<br> * integrated with dialogs must independently listen for an<br> * `editor:dialogsave` event to save the changes into the contents of their<br> * interface.<br> *<br> * @param {Drupal.Ajax} [ajax]<br> * The Drupal.Ajax object.<br> * @param {object} response<br> * The server response from the ajax request.<br> * @param {Array} response.values<br> * The values that were saved.<br> * @param {number} [status]<br> * The status code from the ajax request.<br> *<br> * @fires event:editor:dialogsave<br> */<br> Drupal.AjaxCommands.prototype.editorDialogSave = function (<br> ajax,<br> response,<br> status,<br> ) {<br> $(window).trigger('editor:dialogsave', [response.values]);<br> };</pre><p>At this point it would not be a stretch to add additional dialog action buttons for Re-generate, Save to (prompt entity?), etc. This should help drive innovation around this instead of force everyone to learn CKEditor 5 JS API layer which is not really happening at large.</p>
<h3 id="summary-remaining-tasks">Remaining tasks</h3>
<p>tbd</p>
<h3 id="summary-ui-changes">User interface changes</h3>
<p>The CKEditor AI drop down would look the same, but the option list would be derived from registered plugins and not hardcoded.</p>
<h3 id="summary-api-changes">API changes</h3>
<p>tbd</p>
<h3 id="summary-data-model-changes">Data model changes</h3>
<p>tbd</p>
issue