(Mabol~)AI for Resource, Text, Image Generation, Image Vision, Audio Reading (TTS), Translation and Questions. Same helps, opinion and improvements

(I had already written a post but accidentally deleted it, mannaccia Eva :grimacing:)

Hi, for a couple of years now to create resources on some sites without copywriters or graphic designers, I used to open 3 or 4 pages linked to some AI to generate it all.
I started developing before ModAi and AiKit came out, but by then I had started my marathon and did not stop, also because my structure is quite different.
I never studied how to create an Extra with MODx and I am quite allergic to ExtJs, but thanks to what @digitalime shared (infinite thanks), I developed a plugin that would ‘do what I did’.
Basically, the plugin generates a tab in the resource with all possible generation actions for AI.
Generate the content of the resource from 0, generate all its fields (pagetitle, longtitle, alias, introtext, description) even with SeoSuite installed.
Generate all text TVs linked to the resource either as simple text, as textarea or as richtext (only tinyMce and tinyMce RTE).
It is also possible to improve all the values described above, in case they have already been inserted.
Generate any linked images as tv (image, imageplus and galleryitem), followed by their respective alt text, and title, and renames the image file.
Generate an audio reading of the fields one selects and finally translates all the above into various languages.

To do all this, I implemented the following Ai: OpenAi, Mistral, Claude, Gemini, Stability, GetImg, DeepL and ElevenLabs each with their respective parameters and models. The way I have structured everything, inserting new IAs is very quick and easy (this will be better understood in the code section)

I am writing to seek solutions to some issue, to get your opinion and above all for any possible improvements.

Before I go on with the details, I’ll post a B-video that will become a cult :grinning:, 20 minutes of keystrokes, a cough and AI-generated final greetings, an immense ball, and to make it bigger I’ve also left the waiting times for a response from the AI.
With the video it is much better and I have put shortcuts with timing in the description to make it even cooler :grinning:
In 14.30 minutes I generate from 0 a resource and its translation, of these minutes about 8 minutes are waiting for the response of the chosen Ai and can be considerably reduced depending on the AI, the models and the version (free or paid).
In the video:
00:00 generate the content from 0 with some seed, 3:30 generate al resource’s field and 3 Text TV (text, textarea and richtext), 5:50 Generate 3 Image Tv (image, imageplus and galleryitem), 8:50 Vision of Image for Alt Text, Title and name of image, 10:20 Audio Reading (TTS) of the resource (from title only to all fields including TVs), 11:24 the translation of all texts in the resource and images.
At minute 14:55 the result in Italian and at 15:38 the result in English.
Pay no attention to the template or graphics that have been made to show what has been generated as a whole.
Next, from minute 16:22, an exploration of all the features with final greetings from the AI.

Direct link to YouTube with timing in description to jump to desired points
I will now prepare the other topics to post with the detailed description, and especially the problems, otherwise this topic would become too long, as it already is (sorry).

LOCALIZATION and PROMPTS
All MabolAi (let’s call it that we’ll make it quicker ) is localized, for now only in Italian and English, with two files mabolai.inc.php and mabolprompt.inc.php (which one can translate into one’s own language if one wants).
The system/developer prompts are in English even though the language is Italian, despite saying that the AIs, first translate and then respond, with the English version they transpose much better.
The default prompts are language phrases that one can freely change, also there are specific phrases depending on the AI chosen as seen in the following code that calls them (for now the specific prompts are empty but one at will can populate them).

<div contenteditable class="prompt_ini mai-textarea" >'.$modx->lexicon('mabolprompt.ini_gen_campi_titolo').$modx->lexicon('mabolprompt.ini_gen_campi_titolo_claude').'</div>

All MabolAi prompts, fish the default values from the language phrases, but can be directly modified, adapted, improved in the TAB before making the request to the AI ( they are in div contenteditable).
AI text responses from AI are also contained in contenteditable div and thus can be edited before being copied and setting their respective fields.

ARTIFICIAL INTELLIGENCE AI
AIs, out there, are a jungle, between those who do things themselves, those who call out others, those who don’t specify parameters and use standards that are also very different from each other.
I used the ones I knew and some specific ones after some research, as mentioned at the beginning and as you will see below, entering new ones is quite easy and quick.

Each AI I have implemented has its own peculiarities, and I have used almost all of the parameters made available by the same that can be found in their respective documentation.
The parameters, of each AI, are all settable and have default values.
Once set, the parameter column can be closed for a more immediate display of requests and responses.
Now I will explain why I chose the Ai that I implemented. For each one I specify any peculiarities and the models and parameters used.

OPEN AI
It does just about everything, Text, Vision, Image, TTS audio and Translation. Quite inexpensive and prices depend on the model one decides to use.
I still haven’t figured out if there is a free version and what you can do with it because I immediately put 5€ for 1 million tokens (unit of measurement of the answer).
I used it to generate resource content, resource fields, text tv, improve text, generate images, vision images, TTS audio, translation, and for questions, for everything in short.
For Text, Translation and Questions
2 endpoint (service): chat/completition and response
5 model: gpt-4o, gpt-4o-mini, o3-mini, o1 and gpt-4.5-preview (more expansive)
There is an option to use 2 prompts and choose respective roles (developer or user)
Offering the possibility of getting multiple (n) answers not just one
Other parameters: store, max_completion_tokens, temperature, top_p, frequency_penalty, presence_penalty
For Vision
3 model: gpt-4o, gpt-4o-mini and gpt-4.5-preview
Other parameters: detail, max_completion_tokens
For Image
2 model: dall-e-2, dall-e-2
Other parameters (depends on the model): response_format, size, quality, style, n (response)
For Audio TTS
3 model: gpt-4o-mini-tts, tts-1, tts-1-hd
Other parameters: response_format, voice, speed

MISTRAL
The European Ai, currently all free, used for: Text, Vision and Translation. Great for its codestral model dedicated to code ( and more)
I used it to generate resource content, resource fields, text tv, improve text, vision, translation, and for questions.
For Text, Translation and Questions
5 model: Mistral Large, Mistral Small, Pixtral Large, Pixtral and Codestral (specific for code)
There is an option to use 2 prompts and choose respective roles (system or user)
Offering the possibility of getting multiple (n) answers not just one
Other parameters: max_tokens, temperature, top_p, frequency_penalty, presence_penalty
For Vision
3 model: Mistral Small, Pixtral Large, Pixtral
Other parameters: max_tokens

CLAUDE
Another famous AI, which is getting a good share of users, used for: Text, Vision and Translation.
Within certain limits some things are free, for the rest you have to pay but again cheap for the uses of generating a website.
I used it to generate resource content, resource fields, text tv, improve text, vision, translation, and for questions.
For Text, Translation and Questions
4 model: 3.7 Sonnet (the last one), 3.5 Sonnet, 3.5 Haiku and 3 Haiku
There is an option to use 2 prompts and choose to use System prompt
Other parameters: max_tokens, temperature, top_p
For Vision
4 model: 3.7 Sonnet (the last one), 3.5 Sonnet, 3.5 Haiku and 3 Haiku
Other parameters: max_tokens

GEMINI
Google’s AI, for me quite messy in account management, documnetation, but used to Text, Vision, Image and Translation. Among the various offers, it’s practically all free and you do many sites before you run out of free credits
I used it to generate resource content, resource fields, text tv, improve text, generate images, vision images, translation, and for questions, for everything in short.
For Text, Translation and Questions
5 model: 2.0 Flash, 2.0 Flash Lite, 1.5 Flash, 1.5 Flash 8b and 1.5 Pro
There is an option to use 2 prompts and choose to use system_instruction
Offering the possibility of getting multiple (candidateCount) answers not just one
Other parameters: maxOutputTokens, temperature, topP, frequencyPenalty, presencePenalty
For Vision
4 model: 2.0 Flash, 2.0 Flash Lite, 1.5 Flash and 1.5 Pro
Other parameters: maxOutputTokens
For Image
2 model: Imagen 3.0, 2.0 Flash Exp
Other parameters (depends on the model): aspect_ratio, outputOptions, number_of_images

STABILITY
Specialized only in images and therefore used only for Image generation. In my opinion very good and offers the possibility to select the style of the generated image
I used it only for generate images.
For Image
3 endpoint (service): St. Diffusion 3.5, St. Image Core and St. Image Ultra
3 model only for St. Diffusion 3.5: 3.5 Large, 3.5 Large Turbo and 3.5 Medium
Other parameters (depends on the endpoint): response_format, output_format, cfg_scale, aspect_ratio, style_preset (The style of the generated image. e.g. photographic)

GETIMG
This one also specializes only in images. It can invoke other AIs like Stability above but I only used its native models, used only for Image generation. Only for a fee (I think) although cheap, it offers some parameters and under certain circumstances gives good results.
I used it only for generate images.
For Image
2 model: FLUX.1 [schnell] and V2 Essential
Other parameters (depends on the model): response_format, output_format, steps, Width, Height, aspect_ratio, style (The style of the generated image, only 3)

DEEPL
Specializing in translations and written text improvement, used for Text and Translation. In my opinion the best as far as translations are concerned, especially for formatted text, given the parameters it offers. Free translations up to 500K characters per month, while you pay for the improvement of text written.
I used it only for Improve Text and Translation.
For Improve Text
Other parameters: writing_style and tone which are used to customize text enhancement
For Translation
You have to choose the language of the original text and of the translation (there are not all of them, but the main ones)
Other parameters: model_type, preserve_formatting, formality, tag_handling, ignore_tags

ELEVEN LABS
I was looking for an AI that would do audio reading of the text so as not to leave Opena Ai alone, I came across this one almost by accident, used only for TTS audio. It offers an impressive library of voices for you to use, and you can even create a custom one with your own voice.
Free using the default entries and up to 3 specials.
I used it only for TTS audio.
For Audio TTS
2 model: Multilingual v2 and Flash v2.5
Other parameters: voice and output_format

The possibilities between the free ones and the paid ones are many. If you know any Ai to recommend inserting them is really fast, as long as it is not too particular (as you will see in the code part).

Next I will show some special features of the various actions that can be requested from AIs

I explain in more detail the various possibilities of MabolAi, within the relevant tab of the resource.
Unlike the B-Video in the images I use English interface (although this has some problems that will be described in the dedicated part).
As you can see in the following image arriving in the AI, there is the possibility to choose between macro actions which in turn contain sub actions.

CREATE CONTENT
As seen in the image below (A), you can choose the AIs (1) for which you entered apiKey (if there are more than one).
When the AI is chosen, the parameters (2) column is updated with those of the chosen AI.
Both parameter column and prompt can be closed to leave space for viewing/editing the answers obtained.
Both prompt 1 (developer) and prompt 2 can be modified (3) and have as their default value the one taken from the language sentences.
The values (seeds) of the content to be generated (3) can also be changed and, if present, have the values of the resource.



In the image above (B) we see the result of the response (1) and possibly the current value (2).
If AI allows, we may have more than one answer to choose from.
For each answer (if more than one) with the buttons (4) we can simply copy (without formatting) to the clipboard the result or directly set the content of the resource (formatted).
At the bottom (5) is some information about the request.

CREATE (IMPROVE) FIELDS
The difference between resource’s fields creation and improvement is essentially in the prompts and in “improvement” the current value of the field you want to improve is also passed.
In case of field enhancement, ONLY the fields that already have a value will be visible in the select.
In the image below (C), we see that you can select (1) the field to be created (improved).
The select also includes all text tvs associated with the resource of type text, textarea, and richtext.
In case of choosing a TV next to the name the description (2) is also inserted, if it was written when creating the TV



Also in this case (D) you can see the results obtained (1) and the current value (2), if it exists.
In the buttons for each result (3) the copy button (without formatting) is always present and the set button is present ONLY if the field is a classic resource one (pagetitle,longtitle, etc. etc.) or if the text tv is a text, textarea or richtext type.
In case of richtext the set button is present only if tinyMCE or TinyMCE RTE is used.
Here again there is some information about the request (4).

AUDIO TTS
Also in the Audio Reading resource action (i) there is the possibility to select the AI (1) with its parameters (2).
In this case, however, you do not have to enter any prompts but select the resource fields (3).
Among the selectable fields are also text TVs that have a value, in that in hover over the name there will be as a tooltip the eventual description entered for the TV.
For each element chosen, from audio read, you can also enter its label, which in the case of a TV is its caption (or name if absent).


The resulting audio file can be listened to (4) and saved (5) to the folder indicated in the system settings.
If file type TVs are present, you can also set (6) the value of the saved file so that it can be recalled as TV.

With Audio TTS you can also enter free text (L), get the result and save it as a file (1).

TRANSLATION
Also for translation action (M) you can select the Ai (1) and set its parameters (2).
For translation you have to select the field to be translated (3), among the possible fields are all text TVs but also text related to images (alt, title) and the filename of the image.
In addition to the prompt in MabolAI there is the ability to choose the languages (N) of the translation (1). If a language is not present just uncheck (2) and fill in the prompt by hand (3).
Choosing as AI, DeepL (O), you will only be able to edit the parameters (there is no prompt) and you are bound to the languages it supports





The result (P) mirrors the other text type results, in block 1 the results while in 2 the original value.
There are the usual buttons (3) to copy and to set the selected element, whether it is a resource field or a text tv.
As mentioned you can also translate the attributes of an image (for imageplus and galleryitem) and set them in the relevant TV.
You can also translate the name of the image file. In this case a copy of the original is always generated and the new value set in the Image TV field.
Basically some information (4) about the request made.

As with audio, there is also the possibility of free translation (Q), just select AI, set parameters and enter the text to be translated into the prompt.

CREATE IMAGES
For image creation (E) there is the selection of the AI (1) with entry of the respective parameters (2) and seplicemente the prompt sove enter what image you want to generate.
In the generation parameters, depending on the AI, there are many possibilities including the response format or the number of images generated, if the AI allows it



The generated image(s) are shown in the response section, which also contains, as information, the generation time (5).
With the (1) button you can enlarge the image to full screen, while below are the “action” buttons.
With the 2 you can copy the response to base 64, if it was requested that way.
With the 3 you simply save the immgine with a random name in the folder indicated in the system settings.
The 4 keys are only present if there are tvs of type image, imageplus or galleryitem attached to the resource.

VISION IMAGES
With the Image Vision I developed the possibility to create alt text attribute, title, and also the possibility to rename image, which is useful in SEO ambisto.
Also for the Vision (G) there is the possibility to choose the AI (1) and set its parameters.
The prompts in this case are set differently and already present (3) are those for creating alt text, title and for renaming.
As usual they take values from language phrases and can be edited directly here, before making a request.
Also already present are TVs (4) of type image, imageplus or galleryitem present in the resource and with an image already associated, of course.



(H) As I mentioned all the TV images that have a “value” are already present.
For each one there is the caption of the TV (or the name if absent (1)), the description of the TV (2) , if any and some data of the image file (3), name, size and size.
Below are the lines (5) that correspond to the three parameters to be generated, alt text, title and new file name. They contain the current value which may also be absent, if present it will be in light gray and smaller.
As you can see, the galleryitem type TV does not have the ability to rename the image file, this is due to how the Extra Gallery is developed.
For each row there is the element generation key (4), which sends the image, its prompt and the parameters of the Ai chosen.
The column (6) of each element contains the respective action keys, if they are clicked with the field empty or unmodified they generate an error without taking action.
The copy button is always present and copies the value generated by Ai (remember that generated values can be edited directly).
The setting buttons on the other hand are present ONLY there is a corresponding value in the TV type, so only for imageplus and galleryitem. For image you will have to copy it and put it in the template, as TV type does not have specific fields for alt and title fields.
Last of the action buttons is the one that renames the file and obviously resets the TV value to show the “new” file.
In this case you can create a copy with the new name or rename the existing one. In case you want to rename the existing one I warn with a warning (7) and a confirm, because the image could be used in other areas of the template and not just for the resource you are editing.

AI RESPONDE
With MabolAi it is also possible to ask free questions (R) without constraints and get the respective answer.
Again, you select the AI, set the parameters, and proceed to formulate the question while waiting for the answer.
There is an option to set the double prompt if you also want to send the request as system/developer/system_instruction in addition to user.



The response (S) will contain only the result (1) without the current value.
There will be only the ability to copy (2) the result (without formatting) and there will be some information (3) about the request.

TO DO

  • Image selection
    I would like to be able to implement the ability to select an image from the site assets, especially to create alt text. title and possibly rename the file.
    This could also be useful to send a “mask” image when editing an image.
    To do this one would have to use MODx.browser but not if if it is possible, I still have to look into it. In cso anyway I will come up with something manually.
    If anyone has ideas they are obviously all extramega useful
    Current status: 2%

  • Image editing
    Already working on it but the biggest stumbling block here is the AI, you don’t understand very much (so far I’ve only done Stability).
    Often then you need an image to use as a mask parameter and still I should do the “Image choice” part.
    Current status: 25%

  • Video
    Creating videos also tickles me, but in this case I haven’t started anything yet, not even in my head
    Current status: 0%.

If you have ideas, improvements, changes, additions or whatever, I welcome them with open arms.

HOW IT WORKS and CODE
As mentioned the whole thing is localization with two language files to which one can add one’s own language if desired.
The system/developer/system_instruction prompts (prompt 1) I still left them in English.
There are a css file, mabol.css and two js files mabol.js and showdown2.1.0.min.js (for Markdown reading) that are loaded by the plugin.

System Settings
There are some system settings for MabolAi to work:
The apiKeys: mabolclaudekey, maboldeeplkey, mabolelevenkey, mabolgeminikey, mabolgetimgkey, mabolmistralkey, mabolopenaikey, mabolstability_key contain the values of the keys you obtained by registering on the corresponding AI.
If not even one is present nothing will work, reporting the problem.
mabol_anthropic_version
The version of Anthropic used for Claude. Entered as a setting because if it varies just change it from here.
mabol_def_lang_from
The default, editable value of the source text language for translations.
mabol_def_lang_to
The default, editable value of the language into which you want to translate.
mabol_eleven_voice
ID and description of the voices you want to use when using Eleven LAbs for TTS audio.
mabol_gall_album_img_ai
ID of the default Gallery album where to save the generated images.
mabol_path_audio_ai
Path to the folder where to save the generated audio files.
mabol_path_img_ai
Path to the folder where to save the generated images.
mabol_always_base_encoding
Yes/NO, decides whether to send images always encoded in base 64. Mandatory, YES, when working locally.
mabol_url_fun_mabolai
Url (endpoint) where MabolAi functions are located, which are used for AI calls and other purposes.
mabol_version_deepl
The version of AI DeepL you use, Free or Pro

The Tab
From Form Customization you have to create a new rule to which you can possibly give restriction by user groups.
In the rule, to create/edit resource, create a new zone ID (mabol-utilizza-ai) with the name you want. It will be the tab where everything is contained.

Plugin
The plugin is actually very simple because it makes snippets do everything, also to make it more modular and make it easily editable.
The first part of the plugin, loads the files you need, especially the language files to use the nomenclature inside the js functions.

//Prendo eventi di sistema
$eventName = $modx->event->name;

//Esco se non OnDocFormRender
if ($modx->event->name !== 'OnDocFormRender') {
    return;
}

//Archivi linguaggio
$arcLing = ['mabol:mabolai','mabol:mabolprompt'];
//iclo su array per caricare tutto	
for ($i=0; $i<count($arcLing); $i++){
	//Carico per plugin, non serve, sembra
	//$modx->lexicon->load($arcLing[$i]);
	//carico per js
	$modx->controller->addLexiconTopic($arcLing[$i]);
}
// Carico i file che mi servono
$modx->regClientCSS($modx->getOption('assets_url') . 'css/mabolai.css');
$modx->regClientStartupScript($modx->getOption('assets_url') . 'js/showdown2.1.0.min.js');
$modx->regClientStartupScript($modx->getOption('assets_url') . 'js/mabolai.js');

Next, AI keys are taken and it does a check to see if at least one is present (otherwise error)

//Prendiamo tutte le APIKey inserite nelle impostazioni di sistema
$apiKeyOpenAi = $modx->getOption('mabol_openai_key', null, false);	
$apiKeyMistralAi = $modx->getOption('mabol_mistral_key', null, false);
$apiKeyGeminiAi = $modx->getOption('mabol_gemini_key', null, false);
$apiKeyClaudeAi = $modx->getOption('mabol_claude_key', null, false);
$apiKeyDeeplAi = $modx->getOption('mabol_deepl_key', null, false);
$apiKeyGetimgAi = $modx->getOption('mabol_getimg_key', null, false);
$apiKeyStabilityAi = $modx->getOption('mabol_stability_key', null, false);
$apiKeyElevenAi = $modx->getOption('mabol_eleven_key', null, false);
//Se nessuna esiste errore. Per esistenza singola controllo nei vari snippet
if (!$apiKeyOpenAi && !$apiKeyMistralAi && !$apiKeyGeminiAi && !$apiKeyClaudeAi && !$apiKeyDeeplAi && !$apiKeyGetimgAi  && !$apiKeyStabilityAi  && !$apiKeyElevenAi) {
	$modx->regClientStartupHTMLBlock('
		<script type="text/javascript">
			 Ext.onReady(function() {
									var aiActionsTab = Ext.getCmp("mabol-utilizza-ai");
									if (aiActionsTab) {
										aiActionsTab.update(`<div id="erroreTot">'.$modx->lexicon('mabolai.err_nokey').'.</div>`);
			 }});
		</script>
		');
	//$modx->log(MODX_LOG_LEVEL_ERROR, $modx->lexicon('mabolai.err_nokey').'.');
	return ;
}

Then set placeholders that identify the presence of the Keys and are used in the snippets.

//Guardo se posso utilizzare le chiavi e setto placeholder
$apiOpenAi = $apiKeyOpenAi? true:false;
$apiMistralAi = $apiKeyMistralAi? true:false;
$apiGeminiAi = $apiKeyGeminiAi? true:false;
$apiClaudeAi = $apiKeyClaudeAi? true:false;
$apiDeeplAi = $apiKeyDeeplAi? true:false;
$apiGetimgAi = $apiKeyGetimgAi? true:false;
$apiStabilityAi = $apiKeyStabilityAi? true:false;
$apiElevenAi = $apiKeyElevenAi? true:false;
//Setto i placeholder di tutte le api key
$modx->setPlaceholder('apiOpenAi',$apiOpenAi);
$modx->setPlaceholder('apiMistralAi',$apiMistralAi);
$modx->setPlaceholder('apiGeminiAi',$apiGeminiAi);
$modx->setPlaceholder('apiClaudeAi',$apiClaudeAi);
$modx->setPlaceholder('apiDeeplAi',$apiDeeplAi);
$modx->setPlaceholder('apiGetimgAi',$apiGetimgAi);
$modx->setPlaceholder('apiStabilityAi',$apiStabilityAi);
$modx->setPlaceholder('apiElevenAi',$apiElevenAi);

At this point it takes the resource and starts the real construction of the tab html block with its functionality. The first part overwrites the save button, reloading the resource (if it exists), otherwise saving without reloading I didn’t know how to make the related fields update. Procedure that is very fast anyway.

$resource = $modx->controller->resource;        
$modx->regClientStartupHTMLBlock('
	<script type="text/javascript">
		//Sovrascrivo bottone save per ricaricare la pagina
		Ext.override(MODx.panel.Resource, {
            originalSuccess: MODx.panel.Resource.prototype.success
            , success: function (o) {
                this.originalSuccess(o);
				var ricarica = ' . $resource->get('id') . ';
				if (ricarica) {
					var url = location.href, i = url.indexOf("?") + 3;
					MODx.loadPage(url.substr(i));
				}
            }
        });
		//La tab di MABOL-AI creata

The tab is created, if it exists, and first creates the various buttons, with macroactions and subactions

//La tab di MABOL-AI creata
		Ext.onReady(function() {
			var aiActionsTab = Ext.getCmp("mabol-utilizza-ai");
			if (aiActionsTab) {
				aiActionsTab.update(`
					<div id="mabolai-loader"><div class="cont-loader"><div class="cont-loader-cont"><div class="loader"></div><div class="loader-test">'.$modx->lexicon('mabolai.loader_text').'</div></div></div></div>
					<div class="mabolai-actions-box">
						<div id="mabolai-mess"></div>
										
						<h3>'.$modx->lexicon('mabolai.ai_actions').' "<span class="nomeres">' . $resource->get('pagetitle') . '</span>" <span class="idres">(' . $resource->get('id') . ')</span></h3>
						<span class="descPan">'.$modx->lexicon('mabolai.ai_actions_desc').'</span>
						<div class="contBott">
							<button class="bottCatSel" type="button" onclick="MabolAI.apriCat(this,\'text_gen\')">'.$modx->lexicon('mabolai.text_gen').'</button>
							<button class="bottCatSel" type="button" onclick="MabolAI.apriCat(this,\'img_gen\')">'.$modx->lexicon('mabolai.img_gen').'</button>
							<button class="bottCatSel" type="button" onclick="MabolAI.apriCat(this,\'audio_gen\')">'.$modx->lexicon('mabolai.audio_gen').'</button>
							<button class="bottCatSel" type="button" onclick="MabolAI.apriCat(this,\'trad_gen\')">'.$modx->lexicon('mabolai.trad_gen').'</button>
							<button class="bottCatSel" type="button" onclick="MabolAI.apriCatBox(this,\'text_free\')">'.$modx->lexicon('mabolai.gen_text_free').'</button>
						</div>
						<div id="text_gen" class="contBott catBott">
							<button class="bottCat" type="button" onclick="MabolAI.apriBox(this,\'gen_cont\')">'.$modx->lexicon('mabolai.gen_cont').'</button>
							<button class="bottCat" type="button" onclick="MabolAI.apriBox(this,\'gen_campi\')">'.$modx->lexicon('mabolai.gen_campi').'</button>
							<button class="bottCat" type="button" onclick="MabolAI.apriBox(this,\'mig_campi\')">'.$modx->lexicon('mabolai.gen_cont_mig').'</button>
						</div>
						<div id="img_gen" class="contBott catBott">
							<button class="bottCat" type="button" onclick="MabolAI.apriBox(this,\'gen_img\')">'.$modx->lexicon('mabolai.gen_img').'</button>
							<button class="bottCat" type="button" onclick="MabolAI.apriBox(this,\'mod_img\')">'.$modx->lexicon('mabolai.gen_img_mod').'</button>
							<button class="bottCat" type="button" onclick="MabolAI.apriBox(this,\'desc_img\')">'.$modx->lexicon('mabolai.gen_img_alt').'</button>
						</div>
						<div id="audio_gen" class="contBott catBott">
							<button class="bottCat" type="button" onclick="MabolAI.apriBox(this,\'audio_ris\')">'.$modx->lexicon('mabolai.gen_audio_ris').'</button>
							<button class="bottCat" type="button" onclick="MabolAI.apriBox(this,\'audio_free\')">'.$modx->lexicon('mabolai.gen_audio_free').'</button>
						</div>
						<div id="trad_gen" class="contBott catBott">
							<button class="bottCat" type="button" onclick="MabolAI.apriBox(this,\'trad_ris\')">'.$modx->lexicon('mabolai.gen_trad_ris').'</button>
							<button class="bottCat" type="button" onclick="MabolAI.apriBox(this,\'trad_free\')">'.$modx->lexicon('mabolai.gen_trad_free').'</button>
						</div>

Now, for each maxiblock action, I call up macrocreation and control snippets, which all start with sAa

<!-- TEXT TEXT TEXT TEXT TEXT TEXT TEXT TEXT TEXT TEXT TEXT TEXT TEXT TEXT TEXT TEXT TEXT TEXT TEXT TEXT TEXT TEXT TEXT TEXT TEXT TEXT TEXT TEXT TEXT TEXT TEXT TEXT TEXT TEXT TEXT -->
						<div id="gen_cont" class="boxGen">
							<div class="titoloAi" >'.$modx->lexicon('mabolai.gen_cont_desc').'</div> 
							'.$modx->runSnippet('sAaGenCont').'
						</div>
						
						<div id="gen_campi" class="boxGen">
							<div class="titoloAi" >'.$modx->lexicon('mabolai.gen_campi_desc').'</div>
							'.$modx->runSnippet('sAaGenCampi').'
						</div>
						
						<div id="mig_campi" class="boxGen">
							<div class="titoloAi" >'.$modx->lexicon('mabolai.gen_cont_mig_desc').'</div>
							'.$modx->runSnippet('sAaMigCampi').'
						</div>
						
						
						<!-- IMMAGINI IMMAGINI IMMAGINI IMMAGINI IMMAGINI IMMAGINI IMMAGINI IMMAGINI IMMAGINI IMMAGINI IMMAGINI IMMAGINI IMMAGINI IMMAGINI IMMAGINI IMMAGINI IMMAGINI IMMAGINI IMMAGINI IMMAGINI -->
						<div id="gen_img" class="boxGen">
							<div class="titoloAi" >'.$modx->lexicon('mabolai.gen_img_desc').'</div>
							'.$modx->runSnippet('sAaGenImg').'
						</div>
						
						<div id="mod_img" class="boxGen">
							<div class="titoloAi" >'.$modx->lexicon('mabolai.gen_img_mod_desc').'</div>
							'.$modx->runSnippet('sAaModImg').'
						</div>
						
						<div id="desc_img" class="boxGen">
							<div class="titoloAi" >'.$modx->lexicon('mabolai.gen_img_alt_desc').'</div>
							'.$modx->runSnippet('sAaDescImg').'
						</div>
						
						
						<!-- AUDIO AUDIO AUDIO AUDIO AUDIO AUDIO AUDIO AUDIO AUDIO AUDIO AUDIO AUDIO AUDIO AUDIO AUDIO AUDIO AUDIO AUDIO AUDIO AUDIO AUDIO AUDIO AUDIO AUDIO AUDIO AUDIO AUDIO AUDIO AUDIO AUDIO -->
						<div id="audio_ris" class="boxGen">
							<div class="titoloAi" >'.$modx->lexicon('mabolai.gen_audio_ris_desc').'</div>
							'.$modx->runSnippet('sAaAudioRis').'
						</div>
						<div id="audio_free" class="boxGen">
							<div class="titoloAi" >'.$modx->lexicon('mabolai.gen_audio_free_desc').'</div>
							'.$modx->runSnippet('sAaAudioFree').'
						</div>
						
						
						<!-- TRADUZIONE TRADUZIONE TRADUZIONE TRADUZIONE TRADUZIONE TRADUZIONE TRADUZIONE TRADUZIONE TRADUZIONE TRADUZIONE TRADUZIONE TRADUZIONE TRADUZIONE TRADUZIONE TRADUZIONE TRADUZIONE TRADUZIONE -->
						<div id="trad_ris" class="boxGen">
							<div class="titoloAi" >'.$modx->lexicon('mabolai.gen_trad_ris_desc').'</div>
							'.$modx->runSnippet('sAaTradRis').'
						</div>
						
						<div id="trad_free" class="boxGen">
							<div class="titoloAi" >'.$modx->lexicon('mabolai.gen_trad_free_desc').'</div>
							'.$modx->runSnippet('sAaTradFree').'
						</div>	
						
						
						<!-- AI RISPONDE AI RISPONDE AI RISPONDE AI RISPONDE AI RISPONDE AI RISPONDE AI RISPONDE AI RISPONDE AI RISPONDE AI RISPONDE AI RISPONDE AI RISPONDE AI RISPONDE AI RISPONDE AI RISPONDE AI RISPONDE AI RISPONDE -->						
						<div id="text_free" class="boxGen">
							<div class="titoloAi" >'.$modx->lexicon('mabolai.gen_text_free_desc').'</div>
							'.$modx->runSnippet('sAaTextFree').'
						</div>
					</div>

and that’s it for the plugin. Next, the work is done by the snippets.

Snippets sAa
These are the snippets of macro actions and that sort to other snippets. They are all very similar although the translation ones have a block that creates the language selections
They start by checking, by taking placeholders, whether the apiKeys of the AIs that are enabled for action exist. If none exist it reports error

//ARRAY Ai se ci sono e quelle che posso utilizzare con questa azione. DECIDO ORDINE
$usaAI = [];
if ($modx->getPlaceholder('apiOpenAi')){$usaAI["aiOpen"] = "Open AI,Open";}
if ($modx->getPlaceholder('apiMistralAi')){$usaAI["aiMistral"] = "Mistral AI,Mistral";}
if ($modx->getPlaceholder('apiGeminiAi')){$usaAI["aiGemini"] = "Gemini AI,Gemini";}
if ($modx->getPlaceholder('apiClaudeAi')){$usaAI["aiClaude"] = "Claude AI,Claude";}
//Numero di AI
$nAi = count($usaAI);
//Se array vuoto ritrono div di errore
if (!$nAi) {
	$output = '<div id="erroreTot">'.$modx->lexicon('mabolai.err_nokey_action').'.</div>';
	return $output;
}

After that it creates the select for Ai choice and builds the boxAI blocks of the various enabled Ai that have Key

//Una sola AI, sel disabilitata ed inserisco la div corripondente. Else ci sono più ai
if ($nAi == 1) {
	$idAi = key($usaAI);
	$aVal = explode(",", $usaAI[$idAi]);
	$nomeAI = $aVal[0];
	$snippetAI = 'sGenCont'.$aVal[1];
	$selDiv = '<div class="contDivSelAi">
					<div class="contTotSelAi">
						<div class="titSelAi" >'.$modx->lexicon('mabolai.sel_ai').'</div>
						<div class="contSelAi" >
							<select class="selAi" name="selAi" id="selAi" disabled>
								  <option value="'.$idAi.'" selected>'.$nomeAI.'</option>
							</select>
						</div>
					</div>
					<div class="aiScelta">'.$nomeAI.'</div>
			   </div>';
	$contTot ='<div class="'.$idAi.' singAi">';
		$contTot .= $modx->runSnippet($snippetAI);
	$contTot .='</div>';
}else{	
	//Inizializzo
	$selDiv = '<div class="contDivSelAi">
					<div class="contTotSelAi">
						<div class="titSelAi" >'.$modx->lexicon('mabolai.sel_ai').'</div>
						<div class="contSelAi" >';
	$n=1;
	foreach ($usaAI as $key => $value) {
		$idAi = $key;
		$aVal = explode(",", $value);
		$nomeAI = $aVal[0];
		$snippetAI = 'sGenCont'.$aVal[1];
		if($n==1){
			$selDiv .= '<select class="selAi" name="selAi" id="selAi" onchange="MabolAI.selAi(this,\'gen_cont\')">';
			$selDiv .= '<option value="'.$idAi.'" selected>'.$nomeAI.'</option>';
			$contTot ='<div class="'.$idAi.' singAi">';
			$contTot .= $modx->runSnippet($snippetAI);
			$nomeAIins = $nomeAI;
		}else if ($n==$nAi){
			$selDiv .= '<option value="'.$idAi.'">'.$nomeAI.'</option>';										
			$selDiv .= '</select></div></div><div class="aiScelta">'.$nomeAIins.'</div></div>';
			$contTot .='<div class="'.$idAi.' singAi aiChiusa">';
		}else{
			$selDiv .= '<option value="'.$idAi.'">'.$nomeAI.'</option>';
			$contTot .='<div class="'.$idAi.' singAi aiChiusa">';
		}
		$contTot .='</div>';
		$n++;
	}					
}
$output = $selDiv.$contTot;
return $output;

The ai-specific boxes, boxAi, are created by launching the AI-specific snippets

$contTot .= $modx->runSnippet($snippetAI);

Some SAa snippets are more elaborate because they have to create other elements in addition to AI select, $selDiv, and $contTot content construction.
For example, sAa’s that involve field selection, $selField, contain a generator of the field select. The most atypical is that of the AUDIO TTS which contains checks for choices:

$resource = $modx->controller->resource;
//Se non c'è id, risorsa in creazione e non posso fare audio risorsa
if (!$resource->get('id')) {
	$output = '<div id="erroreTot">'.$modx->lexicon('mabolai.err_no_risorsa_audio').'</div>';
	return $output;
}
//guardo se ci sono tv file per la div che contiene tutte le possibilità
$divContSetTV = '';
$qTV = $modx->newQuery(modTemplateVarTemplate::class);
$qTV ->leftJoin(modTemplateVar::class,'modTemplateVar', 'modTemplateVar.id = modTemplateVarTemplate.tmplvarid');
$qTV ->select($modx->getSelectColumns(modTemplateVar::class,'modTemplateVar','', ['id','source','type','caption','description']));
$qTV ->where([
				['modTemplateVar.type' => 'file']
			]);
$qTV ->where(['modTemplateVarTemplate.templateid' => $resource->get('template')]);
$iTotTV = $modx->getCount(modTemplateVarTemplate::class,$qTV);
if($iTotTV) {
	$divContSetTV = '<div class="divNasc contSetTV">';
	$oTV = $modx->getIterator(modTemplateVarTemplate::class,$qTV);
	foreach($oTV as $TV) {
		//guardo se c'è descrizione
		$descTV = '';
		if($TV->get('description')){
			$descTV = '<span>'.$TV->get('description').'</span>'; 
		}
		$divContSetTV .= '<div class="singSetTV">
							<div class="nomeTV">' .$TV->get('caption').' '.$descTV.'</div>
							<button type="button" onclick="MabolAI.setTabTv(this,'.$TV->get('id').','.$TV->get('source').',\''.$TV->get('type').'audio\')"><i class="icon fa-long-arrow-alt-right"></i></button>
						</div>';
	}	
	$divContSetTV .= '</div>';
}
//inserisco un input hidden che contiene il valore se è  dare oppure no il ciclo onvhange di selAi per tutti i valori. TO DO in javascript
$selCampo = '<input type="hidden" class="jsDaFare" name="jsDaFare" value="1" />';
//Creo blocco campi da scegliere per audio risorsa, se sono presenti, unico sicuro è titolo
$selCampo .= '<div class="contDivSelCA">
				<div class="contTotSelCA">
					<div class="titSelCA" >'.$modx->lexicon('mabolai.sel_ele_audio').'</div>
					<div class="contSelCA" >';
//Titolo
$selCampo .= '			<div class="contSingCA" >
							<div class="contSingCATit">'.$modx->lexicon('mabolai.titolo').'</div>
							<div class="contSingCACont">
								<input  class="checkCACampo" type="checkbox" checked="checked" data-campo="Titolo" data-mn="valore" onclick="MabolAI.mnCA(this,\'audio_ris\')"><span>'.$modx->lexicon('mabolai.valore').'</span>
							</div>
							<div class="contSingCALabel">
								<input  class="checkCALabel" type="checkbox" checked="checked" data-campo="Titolo" data-mn="label" onclick="MabolAI.mnCA(this,\'audio_ris\')"><span>'.$modx->lexicon('mabolai.label').'</span>
							</div>
						</div>';
//Contenuto
if(!empty($resource->getContent())){
$selCampo .= '			<div class="contSingCA" >
							<div class="contSingCATit">'.$modx->lexicon('mabolai.contenuto').'</div>
							<div class="contSingCACont">
								<input  class="checkCACampo" type="checkbox" checked="checked" data-campo="Cont" data-mn="valore" onclick="MabolAI.mnCA(this,\'audio_ris\')"><span>'.$modx->lexicon('mabolai.valore').'</span>
							</div>
							<div class="contSingCALabel">
								<input  class="checkCALabel" type="checkbox" checked="checked" data-campo="Cont" data-mn="label" onclick="MabolAI.mnCA(this,\'audio_ris\')"><span>'.$modx->lexicon('mabolai.label').'</span>
							</div>
						</div>';
}	
//Sintesi
if($resource->get('introtext')){
$selCampo .= '			<div class="contSingCA" >
							<div class="contSingCATit">'.$modx->lexicon('mabolai.introtext').'</div>
							<div class="contSingCACont">
								<input  class="checkCACampo" type="checkbox" data-campo="Riep" data-mn="valore" onclick="MabolAI.mnCA(this,\'audio_ris\')"><span>'.$modx->lexicon('mabolai.valore').'</span>
							</div>
							<div class="contSingCALabel">
								<input  class="checkCALabel" type="checkbox" data-campo="Riep" data-mn="label" onclick="MabolAI.mnCA(this,\'audio_ris\')"><span>'.$modx->lexicon('mabolai.label').'</span>
							</div>
						</div>';
}
//MetaTitle
if($resource->get('longtitle')){
$selCampo .= '			<div class="contSingCA" >
							<div class="contSingCATit">'.$modx->lexicon('mabolai.meta_title').'</div>
							<div class="contSingCACont">
								<input  class="checkCACampo" type="checkbox" data-campo="MetaTit" data-mn="valore" onclick="MabolAI.mnCA(this,\'audio_ris\')"><span>'.$modx->lexicon('mabolai.valore').'</span>
							</div>
							<div class="contSingCALabel">
								<input  class="checkCALabel" type="checkbox" data-campo="MetaTit" data-mn="label" onclick="MabolAI.mnCA(this,\'audio_ris\')"><span>'.$modx->lexicon('mabolai.label').'</span>
							</div>
						</div>';
}
//MetaDesc
if($resource->get('description')){
$selCampo .= '			<div class="contSingCA" >
							<div class="contSingCATit">'.$modx->lexicon('mabolai.meta_desc').'</div>
							<div class="contSingCACont">
								<input  class="checkCACampo" type="checkbox" data-campo="Desc" data-mn="valore" onclick="MabolAI.mnCA(this,\'audio_ris\')"><span>'.$modx->lexicon('mabolai.valore').'</span>
							</div>
							<div class="contSingCALabel">
								<input  class="checkCALabel" type="checkbox" data-campo="Desc" data-mn="label" onclick="MabolAI.mnCA(this,\'audio_ris\')"><span>'.$modx->lexicon('mabolai.label').'</span>
							</div>
						</div>';
}
//adesso prendo tutte le eventuali variabili di testo che hanno un valore e le inserisco
//guardo se ci sono tv di testo da inserire nella select
$qTVT = $modx->newQuery(modTemplateVarTemplate::class);
$qTVT ->leftJoin(modTemplateVar::class,'modTemplateVar', 'modTemplateVar.id = modTemplateVarTemplate.tmplvarid');
$qTVT ->select($modx->getSelectColumns(modTemplateVar::class,'modTemplateVar','', ['id','type','caption','description']));
$qTVT ->where([
				['modTemplateVar.type' => 'text'], ['OR:modTemplateVar.type:=' => 'textarea'], ['OR:modTemplateVar.type:=' => 'richtext']
			]);
$qTVT ->where(['modTemplateVarTemplate.templateid' => $resource->get('template')]);
$iTotTVT = $modx->getCount(modTemplateVarTemplate::class,$qTVT);
if($iTotTVT) {
	$oTVT = $modx->getIterator(modTemplateVarTemplate::class,$qTVT);
	foreach($oTVT as $TVT) {
		//controllo che la tv abbia un valore altrimenti non la posso migliorare ma devo crearla
		$tvValueT = $modx->getObject(modTemplateVarResource::class, [
		  'tmplvarid' => $TVT->get('id'),
		  'contentid' => $resource->get('id')
		]);
		if ($tvValueT) {
		//guardo se c'è descrizione
			$descTVT = '';
			if($TVT->get('description')){
				$descTVT = '<span>'.$TVT->get('description').'</span>'; 
			}
		  $selCampo .= '<div class="contSingCA" >
							<div class="contSingCATit">' .$TVT->get('caption').' '.$descTVT.'</div>
							<div class="contSingCACont">
								<input  class="checkCACampo" type="checkbox" data-campo="tv'.$TVT->get('id').','.$TVT->get('type').'" data-mn="valore" onclick="MabolAI.mnCA(this,\'audio_ris\')"><span>'.$modx->lexicon('mabolai.valore').'</span>
							</div>
							<div class="contSingCALabel">
								<input  class="checkCALabel" type="checkbox" data-campo="tv'.$TVT->get('id').','.$TVT->get('type').'" data-mn="label" onclick="MabolAI.mnCA(this,\'audio_ris\')"><span>'.$modx->lexicon('mabolai.label').'</span>
							</div>
						</div>';
		}
	}
}
$selCampo .='</div></div></div>';

Another, different and more complex one is for the translation of a resource. It must take, in addition to fields, text tvs and image tvs

//Inizializzo input hidden con path
$inHid = '';
//Inizio la select dei campi da tradurre con selezionato content. Ho controllato contenuto e quindi c'è anche titolo (pagetitle)
$selCampo = '<div class="contDivSelCR">';
$selCampo .= '<div class="contTotSelCR">';
$selCampo .= '<div class="titSelCR" >'.$modx->lexicon('mabolai.sel_campo_ris').'</div>';
$selCampo .= '<div class="contSelCR" >';
$selCampo .= '<select class="selCR" name="selCR" id="selCR" onchange="MabolAI.selCR(this,\'trad_ris\')">';
$selCampo .= '<option value="Cont" selected>'.$modx->lexicon('mabolai.contenuto').'</option>';
$selCampo .= '<option value="Titolo">'.$modx->lexicon('mabolai.titolo').'</option>';
if($resource->get('longtitle')){$selCampo .= '<option value="MetaTit">'.$modx->lexicon('mabolai.meta_title').'</option>';}
if($resource->get('description')){$selCampo .= '<option value="Desc">'.$modx->lexicon('mabolai.meta_desc').'</option>';}
if($resource->get('introtext')){$selCampo .= '<option value="Riep">'.$modx->lexicon('mabolai.introtext').'</option>';}
if($resource->get('alias')){$selCampo .= '<option value="Alias">'.$modx->lexicon('mabolai.alias').'</option>';}

//guardo se ci sono tv di testo da inserire nella select
$qTV = $modx->newQuery(modTemplateVarTemplate::class);
$qTV ->leftJoin(modTemplateVar::class,'modTemplateVar', 'modTemplateVar.id = modTemplateVarTemplate.tmplvarid');
$qTV ->select($modx->getSelectColumns(modTemplateVar::class,'modTemplateVar','', ['id','type','name','caption']));
$qTV ->where([
				['modTemplateVar.type' => 'text'], ['OR:modTemplateVar.type:=' => 'textarea'], ['OR:modTemplateVar.type:=' => 'richtext']
			]);
$qTV ->where(['modTemplateVarTemplate.templateid' => $resource->get('template')]);
$iTotTV = $modx->getCount(modTemplateVarTemplate::class,$qTV);
if($iTotTV) {
	$oTV = $modx->getIterator(modTemplateVarTemplate::class,$qTV);
	foreach($oTV as $TV) {
		//controllo che la tv abbia un valore altrimenti non la posso migliorare ma devo crearla
		$tvValue = $modx->getObject(modTemplateVarResource::class, [
		  'tmplvarid' => $TV->get('id'),
		  'contentid' => $resource->get('id')
		]);
		if ($tvValue) {
			$nomeTv = $TV->get('caption')?$TV->get('caption'):$TV->get('name');
			$selCampo .= '<option value="tv'.$TV->get('id').','.$TV->get('type').'">TV: '.$nomeTv.'</option>';
		}
	}
}

//Adesso devo fare la stessa cosa però con le tv Immagine per tradurre alt, title o addirittura nome immagine
$qTVImg = $modx->newQuery(modTemplateVarTemplate::class);
$qTVImg ->leftJoin(modTemplateVar::class,'modTemplateVar', 'modTemplateVar.id = modTemplateVarTemplate.tmplvarid');
$qTVImg ->select($modx->getSelectColumns(modTemplateVar::class,'modTemplateVar','', ['id','source','type','name','caption','description']));
$qTVImg ->where([
				['modTemplateVar.type' => 'image'], ['OR:modTemplateVar.type:=' => 'imageplus'], ['OR:modTemplateVar.type:=' => 'galleryitem']
			]);
$qTVImg ->where(['modTemplateVarTemplate.templateid' => $resource->get('template')]);
$iTotTVImg = $modx->getCount(modTemplateVarTemplate::class,$qTVImg);
if($iTotTVImg) {
	$oTVImg = $modx->getIterator(modTemplateVarTemplate::class,$qTVImg);
	foreach($oTVImg as $TVImg) {
		//controllo che la tv abbia un valore altrimenti non posso tradurre i dati
		$tvValueImg = $modx->getObject(modTemplateVarResource::class, [
		  'tmplvarid' => $TVImg->get('id'),
		  'contentid' => $resource->get('id')
		]);
		if ($tvValueImg) {
			$valueTvImg = $tvValueImg->get('value'); 
			$source = $TVImg->get('source');
			$okSource = true;
			if($source && $source!=1){
				$mediaSource = $modx->getObject('MODX\Revolution\Sources\modMediaSource', $source);
				if(!$mediaSource) { $okSource = false;}
				$msType = $mediaSource->get('class_key');
				if($msType != 'MODX\Revolution\Sources\modFileMediaSource') { $okSource = false;}
				$msProps = $mediaSource->getProperties();
				$msPath = $msProps[ 'basePath' ][ 'value' ];
			}
			//Vari valori
			$nomeTvImg = $TVImg->get('caption')?$TVImg->get('caption'):$TVImg->get('name');
			//dati specifici
			$altTvImg = false;
			$titleTvImg = false;
			$cambiaNomeTvImg = false;
			if($source && $source!=1 && $okSource) {
				if($TVImg->get('type') == 'image') {
					$srcTvImg = '/'.$msPath.$valueTvImg;
					$cambiaNomeTvImg = true;
				}	
				if($TVImg->get('type') == 'imageplus') {
					$arrValueTvImg = json_decode($valueTvImg, true);
					$srcTvImg = '/'.$msPath.$arrValueTvImg['sourceImg']['src'];
					if(isset($arrValueTvImg['altTag']) && $arrValueTvImg['altTag']){$altTvImg = true;}
					if(isset($arrValueTvImg['caption']) && $arrValueTvImg['caption']){$titleTvImg = true;}
					$cambiaNomeTvImg = true;
				}							
			}else{
				if($TVImg->get('type') == 'image') {
					$srcTvImg = '/'.$valueTvImg;
					$cambiaNomeTvImg = true;
				}
				if($TVImg->get('type') == 'imageplus') {
					$arrValueTvImg = json_decode($valueTvImg, true);
					$srcTvImg = '/'.$arrValueTvImg['sourceImg']['src'];
					if(isset($arrValueTvImg['altTag']) && $arrValueTvImg['altTag']){$altTvImg = true;}
					if(isset($arrValueTvImg['caption']) && $arrValueTvImg['caption']){$titleTvImg = true;}
					$cambiaNomeTvImg = true;
				}
				if($TVImg->get('type') == 'galleryitem') {
					$arrValueTvImg = json_decode($valueTvImg, true);
					$srcTvImg = $arrValueTvImg['gal_src'];
					if(isset($arrValueTvImg['gal_description']) && $arrValueTvImg['gal_description']){$altTvImg = true;}
					if(isset($arrValueTvImg['gal_name']) && $arrValueTvImg['gal_name']){$titleTvImg = true;}
				}
			}
			if($okSource){
				if($altTvImg){
					$selCampo .= '<option value="tv'.$TVImg->get('id').','.$TVImg->get('type').',alt">IMG(Alt): '.$nomeTvImg.'</option>';				
				}
				if($titleTvImg){
					$selCampo .= '<option value="tv'.$TVImg->get('id').','.$TVImg->get('type').',title">IMG(Title): '.$nomeTvImg.'</option>';
				}
				if($cambiaNomeTvImg){
					$selCampo .= '<option value="tv'.$TVImg->get('id').','.$TVImg->get('type').',nome">IMG('.$modx->lexicon('mabolai.label_ri_nome').'): '.$nomeTvImg.'</option>';
					$nomeFileTvImg = pathinfo(basename($srcTvImg), PATHINFO_FILENAME);
					$inHid .= '<input type="hidden" class="pathtv'.$TVImg->get('id').'" name="pathtv'.$TVImg->get('id').'" value="'.$srcTvImg.'" />';
					$inHid .= '<input type="hidden" class="nometv'.$TVImg->get('id').'" name="nometv'.$TVImg->get('id').'" value="'.$nomeFileTvImg.'" />';
					$inHid .= '<input type="hidden" class="sourcetv'.$TVImg->get('id').'" name="sourcetv'.$TVImg->get('id').'" value="'.$source.'" />';
				}
			}
		}
	}
}

$selCampo .= '</select></div></div>';
$selCampo .= '<div class="aiCampo">'.$modx->lexicon('mabolai.contenuto').'</div>';
$selCampo .= '</div>';

Snippets AI
These snippets create the AI-specific divs, blocks, boxAi. For the first AI, they are called right away from the sAa snippets.
When the Ai’s select is changed, there is an onchange that starts a js function.
This js function, does a fetch to an internal MabolAi endpoint that calls these snippets with the resource ID.
For this last reason, all AI snippets start with the creation of the resource object, which eventually will be a newObject if you are creating the resource:

use MODX\Revolution\modResource;
//La risorsa
$resource = isset($modx->controller->resource)?$modx->controller->resource:false;
if(!$resource){
	//La risorsa tramite id
	$id = $modx->getOption('id', $scriptProperties);
	$resource = $modx->getObject(modResource::class, $id);
	if(!$resource){$resource = $modx->newObject(modResource::class);}
	//Archivi linguaggio
	$modx->lexicon->load('mabol:mabolai','mabol:mabolprompt');
}

These snippets create the AI-specific divs, blocks, boxAi. For the first AI, they are called right away from the sAa snippets.The initial part, above, also contains the loading of language files that otherwise, because of the round they make, would not be read.

then some variables/blocks are created depending on the cases, such as the current value of a field, default languages for translations, keywords if using SeoSuite.

//Costruisco parte risorsa da visualizzare nel prompt
$risPromptCont = $resource->getContent();
//Costruisco valore attuale da passare
$vaAtt = $resource->getContent();
//numero caratteri del contenuto
$nCarCont = strlen($vaAtt);
//Setto i linguaggi di default
//Valori default
$langDaDef = $modx->getOption('mabol_def_lang_from', null, '');
$langADef = $modx->getOption('mabol_def_lang_to', null, '');
//I Linguaggi da utilizzare nella select, i classici che si usano di più. Scritti in inglese perchè vanno nel prompt developer
$lingue = [
    'IT' => 'Italian',
    'EN' => 'English',
    'EN-GB' => 'British English',
    'EN-US' => 'American English',
    'DE' => 'German',
    'ES' => 'Spanish',
    'ET' => 'Estonian',
    'FI' => 'Finnish',
    'FR' => 'French',
    'HU' => 'Hungarian',
    'LT' => 'Lithuanian',
    'NL' => 'Dutch',
    'PL' => 'Polish',
    'PT-PT' => 'Portuguese',
    'PT-BR' => 'Brazilian Portuguese',
    'RO' => 'Romanian',
    'SK' => 'Slovak',
    'SL' => 'Slovenian',
    'SV' => 'Swedish',
    'UK' => 'Ukrainian',
    'RU' => 'Russian'
];
if(isset($lingue[$langDaDef]) && $lingue[$langDaDef]){$langDa = $lingue[$langDaDef].' ('.$langDaDef.')';}else{$langDa = $modx->lexicon('mabolai.errore');}
if(isset($lingue[$langADef]) && $lingue[$langADef]){$langA = $lingue[$langADef].' ('.$langADef.')';}else{$langA = $modx->lexicon('mabolai.errore');}

$keywords = '';
if(class_exists('Sterc\\SeoSuite\\Model\\SeoSuiteResource')){
$objRes = $modx->getObject('Sterc\\SeoSuite\\Model\\SeoSuiteResource', [
		'resource_id' => $resource->get('id')
	]);
 if ($objRes) {	$keywords = $objRes->get('keywords');}
}

Then the div containing prompts , the one containing parameters and the one containing results are built.
The prompt and parameter divs can be open or closed:

<div class="boxTest" ><div class="boxTestTit" >'.$modx->lexicon('mabolai.prompt').'</div><div class="boxTestFre"  onclick="MabolAI.acPrompt(this,\'gen_campi\')"></div></div>
			<div class="boxTestDesc" >'.$modx->lexicon('mabolai.prompt_desc').'</div>
[...]
<div class="boxTest" ><div class="boxTestTit" >'.$modx->lexicon('mabolai.parama').'</div><div class="boxTestFre"  onclick="MabolAI.acPara(this,\'gen_campi\')"></div></div>
			<div class="boxTestDesc" >'.$modx->lexicon('mabolai.parama_desc').'</div>
[...]
<div class="contInvio" >
		<button type="button" onclick="MabolAI.getDataAI(\'gen_campi\')" >'.$modx->lexicon('mabolai.bott_invia').'</button>
	</div>
[...]
<div class="cont-risu chiusa">
[...]
	</div>
	<div class="mabolai_errore" ></div>

Prompts taken from the language phrases are also generated here.
When an Ai or a field is sampled, the corresponding prompts are loaded

<div class="contPrompt" >
				<div class="errori_prompt"></div>
				<div class="prompt1">'.$modx->lexicon('mabolai.prompt').' 1</div>
				<div contenteditable class="prompt_ini mai-textarea" >'.$modx->lexicon('mabolprompt.ini_gen_campi_titolo').$modx->lexicon('mabolprompt.ini_gen_campi_titolo_mistral').'</div>
				<div class="prompt_dati mai-textarea mai-textarea-dis" title="'.$modx->lexicon('mabolai.title_content').'"  >'.$risPromptCont.'</div>
				<div contenteditable class="prompt_fine mai-textarea" >'.$modx->lexicon('mabolprompt.fine_gen_campi_titolo').$modx->lexicon('mabolprompt.fine_gen_campi_titolo_mistral').'</div>
				<div class="prompt2">'.$modx->lexicon('mabolai.prompt').' 2</div>
			</div>

The most anomalous of this is the one for image vision that contains 3 different prompts depending on the case

<div class="contPrompt" >
				<div class="errori_prompt"></div>
				<div class="contPromptDescImg" >
					<div class="contSingPrompt" >
						<div class="promptTit">'.$modx->lexicon('mabolai.prompt_crea_alt').'</div>
						<div contenteditable class="prompt_desc_img_alt mai-textarea" >'.$modx->lexicon('mabolprompt.desc_img_alt').'</div>
					</div>
					<div class="contSingPrompt" >
						<div class="promptTit">'.$modx->lexicon('mabolai.prompt_crea_title').'</div>
						<div contenteditable class="prompt_desc_img_title mai-textarea" >'.$modx->lexicon('mabolprompt.desc_img_title').'</div>
					</div>
					<div class="contSingPrompt" >
						<div class="promptTit">'.$modx->lexicon('mabolai.prompt_mod_nome').'</div>
						<div contenteditable class="prompt_desc_img_nome mai-textarea" >'.$modx->lexicon('mabolprompt.desc_img_nome').'</div>
					</div>
				</div>
			</div>

The snippetAi, again to make it modular and easier to add AI, also create AI parameters by calling a special snipper (snippet Spec)

<div class="contPara" >
				<div class="errori_para"></div>
				'.$modx->runSnippet('sGenParAi', array('qualeAi' => 'aiClaude','tipoRic' => 'desc_img')).'
			</div>

The snippet is passed the AI and request type and creates the parameter column.

A special case of snippetAi is the one for vision that goes for all TV immages that have a value

<div class="contBloccoDescImg contBloccoTv">
			<h3>'.$modx->lexicon('mabolai.tv_img_risorsa').'</h3>
			<div class="bloccoImgDesc bloccoTv">';
				//Per ogni TV della risorsa creo rispettiva div, se ne ha TV
				$qTV = $modx->newQuery(modTemplateVarTemplate::class);
				$qTV ->leftJoin(modTemplateVar::class,'modTemplateVar', 'modTemplateVar.id = modTemplateVarTemplate.tmplvarid');
				$qTV ->select($modx->getSelectColumns(modTemplateVar::class,'modTemplateVar','', ['id','source','type','name','caption','description']));
				$qTV ->where([
								['modTemplateVar.type' => 'image'], ['OR:modTemplateVar.type:=' => 'imageplus'], ['OR:modTemplateVar.type:=' => 'galleryitem']
							]);
				$qTV ->where(['modTemplateVarTemplate.templateid' => $resource->get('template')]);
				$iTotTV = $modx->getCount(modTemplateVarTemplate::class,$qTV);
				if($iTotTV) {
					$oTV = $modx->getIterator(modTemplateVarTemplate::class,$qTV);
					$haVal = false;
					foreach($oTV as $TV) {
						//controllo che la tv abbia un valore altrimenti non posso modificarne i dati
						$tvValue = $modx->getObject(modTemplateVarResource::class, [
						  'tmplvarid' => $TV->get('id'),
						  'contentid' => $resource->get('id')
						]);
						if ($tvValue) {
							$valueTvImg = $tvValue->get('value');
							if(!$haVal){$haVal = true;}
							$source = $TV->get('source');
							$okSource = true;
							if($source && $source!=1){
								$mediaSource = $modx->getObject('MODX\Revolution\Sources\modMediaSource', $source);
								if(!$mediaSource) { $okSource = false;}
								$msType = $mediaSource->get('class_key');
								if($msType != 'MODX\Revolution\Sources\modFileMediaSource') { $okSource = false;}
								$msProps = $mediaSource->getProperties();
								$msPath = $msProps[ 'basePath' ][ 'value' ];
							}
							//Vari valori
							$nomeTvImg = $TV->get('caption')?$TV->get('caption'):$TV->get('name');
							$descTVImg = '';
							if($TV->get('description')){
								$descTVImg = '<div class="descImgTV">'.$TV->get('description').'</div>'; 
							}
							//dati specifici
							$altTvImg = '';
							$titleTvImg = '';
							if($source && $source!=1 && $okSource) {
								if($TV->get('type') == 'image') {
									$srcTvImg = '/'.$msPath.$valueTvImg;
								}	
								if($TV->get('type') == 'imageplus') {
									$arrValueTvImg = json_decode($valueTvImg, true);
									$srcTvImg = '/'.$msPath.$arrValueTvImg['sourceImg']['src'];
									if(isset($arrValueTvImg['altTag']) && $arrValueTvImg['altTag']){$altTvImg = '<span>'.$arrValueTvImg['altTag'].'</span>';}
									if(isset($arrValueTvImg['caption']) && $arrValueTvImg['caption']){$titleTvImg = '<span>'.$arrValueTvImg['caption'].'</span>';}
								}							
							}else{
								if($TV->get('type') == 'image') {
									$srcTvImg = '/'.$valueTvImg;
								}
								if($TV->get('type') == 'imageplus') {
									$arrValueTvImg = json_decode($valueTvImg, true);
									$srcTvImg = '/'.$arrValueTvImg['sourceImg']['src'];
									if(isset($arrValueTvImg['altTag']) && $arrValueTvImg['altTag']){$altTvImg = '<span>'.$arrValueTvImg['altTag'].'</span>';}
									if(isset($arrValueTvImg['caption']) && $arrValueTvImg['caption']){$titleTvImg = '<span>'.$arrValueTvImg['caption'].'</span>';}
								}
								if($TV->get('type') == 'galleryitem') {
									$arrValueTvImg = json_decode($valueTvImg, true);
									$srcTvImg = $arrValueTvImg['gal_src'];
									if(isset($arrValueTvImg['gal_description']) && $arrValueTvImg['gal_description']){$altTvImg = '<span>'.$arrValueTvImg['gal_description'].'</span>';}
									if(isset($arrValueTvImg['gal_name']) && $arrValueTvImg['gal_name']){$titleTvImg = '<span>'.$arrValueTvImg['gal_name'].'</span>';}
								}
							}
							if(!$okSource){
								$output .= '<div class="singImg">
												<div class="nomeImgTV">'.$nomeTvImg.'</div>
												<div class="noSource">'.$modx->lexicon('mabolai.err_ms_nonsupportata').'</div>
											</div>';
							}else{
								//Dati Img
								$pathTvImg = rtrim(MODX_BASE_PATH, '/') . $srcTvImg;
								$nomeFileTvImg = basename($srcTvImg);
								$nomeEffFileTvImg = '<span>'. pathinfo($nomeFileTvImg, PATHINFO_FILENAME).'</span>';
								$imgSizeKB = round(filesize($pathTvImg) / 1024, 1);
								list($imgWidth, $imgHeight) = getimagesize($pathTvImg);
								$output .= '<div class="singImg">
												<div class="errImgTV"></div>
												<div class="nomeImgTV">'.$nomeTvImg.'</div>
												<div class="imgImgTV"><img src="'.$srcTvImg.'" ></div>
												' .$descTVImg.'
												<div class="datiImgTV">
													'.$modx->lexicon('mabolai.label_file').':<span class="nomefile">'.$nomeFileTvImg.'</span>
													'.$modx->lexicon('mabolai.label_dim').':<span>'.$imgWidth.'x'.$imgHeight.' px</span>
													'.$modx->lexicon('mabolai.label_size').':<span>'.$imgSizeKB.' KB</span>
												</div>
												<div class="azImgTV azImgTvAlt">
													<div class="eleImgTV eleImgTvGenera">
														<div class="eleNomeImgTV">'.$modx->lexicon('mabolai.genera').'</div>
														<button type="button" onclick="MabolAI.getDataAI(\'desc_img\',\'alt\',this)" ><i class="icon fa-magic"></i></button>
													</div>
													<div class="eleImgTV eleImgTvAlt">
														<div class="eleNomeImgTV">'.$modx->lexicon('mabolai.label_alt').'</div>
														<div contenteditable="true" class="risImgTv targeTalt" data-mod="NO" oninput="this.setAttribute(\'data-mod\', \'SI\');">'.$altTvImg.'</div>
													</div>
													<div class="eleImgTV eleImgTvCopia">
														<div class="eleNomeImgTV">'.$modx->lexicon('mabolai.label_copia').'</div>
														<button type="button" onclick="MabolAI.copiaAtt(this)"><i class="icon icon-files-o"></i></button>
													</div>';
													//Imposta solo se non img classica che non ha i campi
												if($TV->get('type') != 'image') {
													$output .= 
													'<div class="eleImgTV eleImgTvImposta">
														<div class="eleNomeImgTV">'.$modx->lexicon('mabolai.label_imposta').'</div>
														<button type="button" onclick="MabolAI.setAtt(this,\'alt\','.$TV->get('id').','.$TV->get('source').',\''.$TV->get('type').'\',\''.$srcTvImg.'\')""><i class="icon fa-long-arrow-alt-right"></i></button>
													</div>';
												}
									$output .= '</div>
												<div class="azImgTV azImgTvTitle">
													<div class="eleImgTV eleImgTvGenera">
														<div class="eleNomeImgTV">'.$modx->lexicon('mabolai.genera').'</div>
														<button type="button" onclick="MabolAI.getDataAI(\'desc_img\',\'title\',this)" ><i class="icon fa-magic"></i></button>
													</div>
													<div class="eleImgTV eleImgTvTitle">
														<div class="eleNomeImgTV">'.$modx->lexicon('mabolai.label_title').'</div>
														<div contenteditable="true" class="risImgTv targeTtitle" data-mod="NO" oninput="this.setAttribute(\'data-mod\', \'SI\');">'.$titleTvImg.'</div>
													</div>
													<div class="eleImgTV eleImgTvCopia">
														<div class="eleNomeImgTV">'.$modx->lexicon('mabolai.label_copia').'</div>
														<button type="button" onclick="MabolAI.copiaAtt(this)"><i class="icon icon-files-o"></i></button>
													</div>';
												//Imposta solo se non img classica che non ha i campi
												if($TV->get('type') != 'image') {
													$output .= 
													'<div class="eleImgTV eleImgTvImposta">
														<div class="eleNomeImgTV">'.$modx->lexicon('mabolai.label_imposta').'</div>
														<button type="button" onclick="MabolAI.setAtt(this,\'title\','.$TV->get('id').','.$TV->get('source').',\''.$TV->get('type').'\',\''.$srcTvImg.'\')""><i class="icon fa-long-arrow-alt-right"></i></button>
													</div>';
												}
									$output .= '</div>';
											//NOME file solo se non galleryitem
											if($TV->get('type') != 'galleryitem') {
												$output .= '
												<div class="azImgTV azImgTvNome">
													<div class="eleImgTV eleImgTvGenera">
														<div class="eleNomeImgTV">'.$modx->lexicon('mabolai.genera').'</div>
														<button type="button" onclick="MabolAI.getDataAI(\'desc_img\',\'nome\',this)" ><i class="icon fa-magic"></i></button>
													</div>
													<div class="eleImgTV eleImgTvNome">
														<div class="eleNomeImgTV">'.$modx->lexicon('mabolai.label_ri_nome').'</div>
														<div contenteditable="true" class="risImgTv targeTnome" data-mod="NO" oninput="this.setAttribute(\'data-mod\', \'SI\');">'.$nomeEffFileTvImg.'</div>
													</div>
													<div class="eleImgTV eleImgTvCopia">
														<div class="eleNomeImgTV">'.$modx->lexicon('mabolai.label_copia').'</div>
														<button type="button" onclick="MabolAI.copiaAtt(this)"><i class="icon icon-files-o"></i></button>
													</div>
													<div class="eleImgTV eleImgTvImposta">
														<div class="eleNomeImgTV">'.$modx->lexicon('mabolai.label_imposta').'</div>
														<button type="button" onclick="MabolAI.setAtt(this,\'nome\','.$TV->get('id').','.$TV->get('source').',\''.$TV->get('type').'\',\''.$srcTvImg.'\')""><i class="icon fa-long-arrow-alt-right"></i></button>
													</div>
												</div>
												<div class="inputCopiaRename"><input  class="checkCopiaRename" type="checkbox" checked="checked" onclick="MabolAI.changeCopiaRename(this)"> '.$modx->lexicon('mabolai.copia_rinominata').'</div>
												<div class="descWarning" style="display:none;">'.$modx->lexicon('mabolai.attenzione_cambio_nome').'</div>
												';
											}					
								$output .= '</div>';
							}
						}	
					}
					if(!$haVal){$output .= '<div class="noImgTV">' .$modx->lexicon('mabolai.desc_img_no_tv').'</div>';}
				}else{
					    $output .= '<div class="noImgTV">' .$modx->lexicon('mabolai.desc_img_no_tv').'</div>';
				}
$output .=' </div>
		    <div class="cont-ai"></div>
	</div>';

Snippet Spec
Special snippets (Snippet Spec), are snippets that are called up to create particular parts of MabolAi, simplify (for me), the addition/editing of an AI.

sGenParAi.php
It generates the AI parameter column depending, precisely, on the ai ($qualeAi) and the request type ($tipoRic).

//OPEN AI
if($qualeAi == 'aiOpen'){
	//TEXT OPEN AI
	if($tipoRic == 'gen_cont' || $tipoRic == 'gen_campi' || $tipoRic == 'mig_campi' || $tipoRic == 'text_free' || $tipoRic == 'trad_ris' || $tipoRic == 'trad_free'){
	
		$output ='
			<div class="singPara" >
				<div class="singParaTit" >Service:</div>
				<div class="contSingRole" >
					<select class="selService" name="service" id="service" onchange="MabolAI.changeParaInput(this,\''.$tipoRic.'\',\'BFC\')">
						  <option value="chat" selected>chat/completions</option>
						  <option value="responses">responses</option>
					</select>
				</div>
				<div class="singParaDesc" ></div>
			</div>
			<div class="singPara" >
				<div class="singParaTit" >model:</div>
				<div class="contSingRole" >
					<select class="selModel" name="model" id="model">
						  <option value="gpt-4o" selected>gpt-4o</option>
						  <option value="gpt-4o-mini">gpt-4o-mini</option>
						  <option value="o3-mini">o3-mini</option>
						  <option value="o1">o1</option>
						  <option value="gpt-4.5-preview">gpt-4.5-preview</option>
					</select>
				</div>
				<div class="singParaDesc" >'.$modx->lexicon('mabolai.para_model_desc').'</div>
			</div>
[...]
        }
	//GEN IMG OPEN AI
	if($tipoRic == 'gen_img'){
[...]
	}
}

//MISTRAL AI
if($qualeAi == 'aiMistral'){
[...]
}

//GEMINI AI
if($qualeAi == 'aiGemini'){
[...]
}

sGetAiRis.php
Snippet that is called by a function inside endpoint inside MabolAi.
Depending on the type of Ai and other parameters builds a unique return value used by js to create the result block

//RITORNO TESTO
if($tipoRic == 'gen_cont' || $tipoRic == 'gen_campi' || $tipoRic == 'mig_campi' || $tipoRic == 'text_free' || $tipoRic == 'trad_ris' || $tipoRic == 'trad_free' || $tipoRic == 'desc_img'){
	//OPEN AI
	if($qualeAi == 'aiOpen'){
		//Gestiamo errori
		if(isset($result['error']['message'])) {
			$risAi['ok'] = false;
			$risAi['error']['message'] = $result['error']['message'];
			return $risAi;
		}
		//Nessun risultato
		if(($miauscita == 'responses' && !isset($result['output'])) || ($miauscita != 'responses' && !isset($result['choices']))) {
			$risAi['ok'] = false;
			$risAi['error']['message'] = $modx->lexicon('mabolai.err_no_gen');
			return $risAi;
		}
		if($miauscita == 'responses'){
			for ($i=0; $i<count($result['output']); $i++){
				$risAi['data'][$i] = $result['output'][$i]['content'][0]['text'];
				//ultimo motivo di stop. qui fallback
				$causaStop =  'STOP';
			}
			$tokens =  $result['usage']['output_tokens'];
		}else{
			for ($i=0; $i<count($result['choices']); $i++){
				$risAi['data'][$i] = $result['choices'][$i]['message']['content'];
				//ultimo motivo di stop
				$causaStop =  $result['choices'][$i]['finish_reason'];
			}
			$tokens =  $result['usage']['completion_tokens'];
		}
		//costruisco la div delle info
		$risAi['info'] = '<div class="ai_model">'.$modx->lexicon('mabolai.ai_model').' <span>'.$result['model'].'</span></div> | 
						  <div class="ai_tipo">'.$modx->lexicon('mabolai.ai_tipo').' <span>'.$result['object'].'</span></div> | 
						  <div class="ai_tokens">'.$modx->lexicon('mabolai.ai_tokens').' <span>'.$tokens.'</span></div> | ';
		if($causaStop != 'stop'){
		$risAi['info'] .= '<div class="ai_stop">'.$modx->lexicon('mabolai.ai_stoppato').' <span>'.$causaStop.'</span></div> | ';		
		}
		$risAi['info'] .= '<div class="ai_tempo">'.$modx->lexicon('mabolai.ai_tempo').' <span>'.$tempo.'</span> '.$modx->lexicon('mabolai.ai_sec').'</div>';
	}
	
	//MISTRAL AI
	if($qualeAi == 'aiMistral'){
         [...]
       }
}
//RITORNO IMMAGINE
else if($tipoRic == 'gen_img'){
[...]
}
[...]
else{
	$risAi['ok'] = false; 
	$risAi['error']['message'] = $modx->lexicon('mabolai.errore').': '.$modx->lexicon('mabolai.no_tipo_ric');
}

sSetParInviaRic.php
This snippet is also called from internal endpoint and goes to create the parameters for actually sending the request to the AI

$qualeAi = $modx->getOption('qualeAi', $scriptProperties);
$tipoRic = $modx->getOption('tipoRic', $scriptProperties);
$modelLink = $modx->getOption('modelLink', $scriptProperties);
$miauscita = $modx->getOption('miauscita', $scriptProperties);

//Array parametri
$parRic = [];

//OPEN AI
if($qualeAi == 'aiOpen'){	
	if($tipoRic == 'gen_cont' || $tipoRic == 'gen_campi' || $tipoRic == 'mig_campi' || $tipoRic == 'text_free' || $tipoRic == 'trad_ris' || $tipoRic == 'trad_free' || $tipoRic == 'desc_img'){
	//TEXT
		//vedo se inviare come chat completitions o response
		if($modelLink =='responses'){
			$parRic['apiLink'] = 'https://api.openai.com/v1/responses';
		}else{
			$parRic['apiLink'] = 'https://api.openai.com/v1/chat/completions';
		}
		$parRic['headers'] = [
							"Content-Type: application/json", 
							"Authorization: Bearer {$modx->getOption('mabol_openai_key')}"
							];
	}else if($tipoRic == 'gen_img'){
	//GEN IMG
		$parRic['apiLink'] = 'https://api.openai.com/v1/images/generations';
		$parRic['headers'] = [
							"Content-Type: application/json", 
							"Authorization: Bearer {$modx->getOption('mabol_openai_key')}"
							];
	}else if($tipoRic == 'audio_ris' || $tipoRic == 'audio_free'){
	//GEN IMG
		$parRic['apiLink'] = 'https://api.openai.com/v1/audio/speech';
		$parRic['headers'] = [
							"Content-Type: application/json", 
							"Authorization: Bearer {$modx->getOption('mabol_openai_key')}"
							];
	}else{
		$parRic['errore'] = true;
	}
}

//MISTRAL AI
if($qualeAi == 'aiMistral'){
[...]
}
[...]

MabolAi FUNCTION (endpoint) mabolAiFun.php
As already specified in the js there are some asynchronous calls to functions contained in an internal endpoint whose folder containing it is indicated in the system settings.
What I used to do with ajax calls I now do with fetches by calling internal endpoints.
Like this:

const response = await fetch(MODx.config.mabol_url_fun_mabolai + 'mabolAiFun.php', {[...]}

In the functions file I had to initialize MODx as if it were externally called:

// Inizializzo MODX
require_once dirname(__FILE__,3) . '/config.core.php';
require_once MODX_CORE_PATH . 'model/modx/modx.class.php';
$modx = new modX();
$modx->initialize('mgr');

Next, I loaded the component’s custom tables, in this case only those of Gallery

$modx->addPackage('Gallery', MODX_CORE_PATH.'components/gallery/model/');

The way it is structured, json or files could come in, I discriminated on how much came in

//discrimino sul tipo di content type. la richiesta è SEMPRE POST
$contentType = $_SERVER['CONTENT_TYPE'];
if($contentType === 'application/json'){
	//json
	//Prendo i valori passatomi
	$postInJson = file_get_contents('php://input');
	$postInArr = json_decode($postInJson, true);
	$postInArr['isJson'] = true;
}else{
	//sicuramente multipart/form-data non ne suo altri
	//Prendo i valori passatomi
	$postInArr = $_POST;
	$postInArr['isJson'] = false;
}

Immediately after that I launched the function in the file with error checks

//Se si arriva alla pagina senza funzione si viene rimandati alla pagina di errore
if(empty($sFunzione)) { 
	$url = $modx->makeUrl($modx->getOption('error_page'));
	$modx->sendRedirect($url);
}

//Se esiste la lancio 
if (function_exists($sFunzione)) {
    $sFunzione($modx,$postInArr);
} else {
	$risAi['ok'] = false; 
	$risAi['error']['message'] = 'Funzione chiamata "'.$sFunzione.'" inesistente.';
	$risAi = json_encode($risAi);
	echo $risAi;
	return;
}

Then there are the functions called by the js such as the one that creates BoxAi function getDivAi($modx,$postInArr) {}, the one that savesfile, etc etc
The most important is definitely the one that makes the real call to Ai (also uses Spec snippet)

function inviaRicAi($modx,$postInArr) {
	//Archivi linguaggio
	$modx->lexicon->load('mabol:mabolai');
	//Prendo parametri
	$qualeAi = (isset($postInArr['qualeAi']) && !empty($postInArr['qualeAi']))?$postInArr['qualeAi']:'';
	$tipoRic = (isset($postInArr['tipoRic']) && !empty($postInArr['tipoRic']))?$postInArr['tipoRic']:'';
	$dataRic = (isset($postInArr['dataRic']) && !empty($postInArr['dataRic']))?$postInArr['dataRic']:'';
	
	//Array dei tipi di richiesta supportati
	$arrTipoRic = ['gen_cont','gen_campi','mig_campi','text_free','gen_img','mod_img','desc_img','audio_ris','audio_free','trad_ris','trad_free','cust_gen'];
	
	//Errori chiamata
	if(empty($qualeAi)) { 
		$risAi['ok'] = false; 
		$risAi['error']['message'] = $modx->lexicon('mabolai.no_ai');
		$risAi = json_encode($risAi);
		header('Content-Type: application/json');
		echo $risAi;
		return;
	}
	if(empty($tipoRic)) { 
		$risAi['ok'] = false; 
		$risAi['error']['message'] = $modx->lexicon('mabolai.no_tipo_ai');
		$risAi = json_encode($risAi);
		header('Content-Type: application/json');
		echo $risAi;
		return;
	}
	if(!in_array($tipoRic, $arrTipoRic)) { 
		$risAi['ok'] = false; 
		$risAi['error']['message'] = $modx->lexicon('mabolai.no_tipo_ric');
		$risAi = json_encode($risAi);
		header('Content-Type: application/json');
		echo $risAi;
		return;
	}
	if(empty($dataRic)) { 
		$risAi['ok'] = false; 
		$risAi['error']['message'] = $modx->lexicon('mabolai.no_data_invio');
		$risAi = json_encode($risAi);
		header('Content-Type: application/json');
		echo $risAi;
		return;
	}
	//Tolgo parametro miauscita e lo aggiungo all'array da inviare se non c'è. Usata anche in InviaRic per stability ai
	if(isset($dataRic['miauscita'])){
		unset($dataRic['miauscita']);
	}else{
		$postInArr['dataRic']['miauscita'] = 'Forza Bologna';
	}
	//Tolgo parametro modelLink e lo aggiungo all'array da inviare se non c'è
	if(isset($dataRic['modelLink'])){
		unset($dataRic['modelLink']);
	}else{
		$postInArr['dataRic']['modelLink'] = 'Forza BFC 1909';
	}
	//echo json_encode($postInArr);
	//Creo i parametri da inviare
	$parRic = $modx->runSnippet('sSetParInviaRic', array('qualeAi' => $qualeAi,'tipoRic' => $tipoRic,'modelLink' => $postInArr['dataRic']['modelLink'],'miauscita' => $postInArr['dataRic']['miauscita']));	
	if(isset($parRic['errore']) && $parRic['errore']) { 
		$risAi['ok'] = false; 
		$risAi['error']['message'] = $modx->lexicon('mabolai.err_gen_parinvio');
		$risAi = json_encode($risAi);
		header('Content-Type: application/json');
		echo $risAi;
		return;
	}	
	
	//DESC IMG DESC IMG DESC IMG uno può scegliere di inviare sempre base 64 con impostazione
	//ATTENZIONE IN LOCALE METTERE SEMPRE SI AD IMPOSTAZIONE
	if($tipoRic == 'desc_img' && $modx->getOption('mabol_sempre_codifica_base')) {
		//prendo url immagine a seconda dei casi
		if($qualeAi == 'aiOpen' && $postInArr['dataRic']['modelLink'] == 'chat') {
			$urlDaConv = $postInArr['dataRic']['messages'][0]['content'][1]['image_url']['url'];			
		}
		else if($qualeAi == 'aiOpen' && $postInArr['dataRic']['modelLink'] == 'responses') {
			$urlDaConv = $postInArr['dataRic']['input'][0]['content'][1]['image_url'];
		}
		else if($qualeAi == 'aiMistral') {
			$urlDaConv = $postInArr['dataRic']['messages'][0]['content'][1]['image_url']['url'];			
		}
		else if($qualeAi == 'aiClaude') {
			$urlDaConv = $postInArr['dataRic']['messages'][0]['content'][1]['source']['url'];			
		}
		else if($qualeAi == 'aiGemini') {
			$urlDaConv = $postInArr['dataRic']['contents']['parts'][0]['fileData']['fileUri'];			
		}
		
		$extDaConv = pathinfo($urlDaConv, PATHINFO_EXTENSION);
		if($extDaConv == 'jpg'){$extDaConv == 'jpeg';}
		$base64Media = 'image/'.$extDaConv;
		$base64Data = 'data:'.$base64Media.';base64,';
		$urlDaConv = rtrim(MODX_BASE_PATH, "/") . parse_url($urlDaConv, PHP_URL_PATH);
		$imgDaConv = file_get_contents($urlDaConv);
		$base64Cod = base64_encode($imgDaConv);	

		if($qualeAi == 'aiOpen' && $postInArr['dataRic']['modelLink'] == 'chat') {
			$dataRic['messages'][0]['content'][1]['image_url']['url'] = $base64Data.$base64Cod;
		}
		else if($qualeAi == 'aiOpen' && $postInArr['dataRic']['modelLink'] == 'responses') {
			$dataRic['input'][0]['content'][1]['image_url'] = $base64Data.$base64Cod;
		}
		else if($qualeAi == 'aiMistral') {
			$dataRic['messages'][0]['content'][1]['image_url']['url'] = $base64Data.$base64Cod;			
		}
		else if($qualeAi == 'aiClaude') {
			unset($dataRic['messages'][0]['content'][1]['source']['type']);
			unset($dataRic['messages'][0]['content'][1]['source']['url']);
			$dataRic['messages'][0]['content'][1]['source']['type'] = "base64";	
			$dataRic['messages'][0]['content'][1]['source']['media_type'] = $base64Media;	
			$dataRic['messages'][0]['content'][1]['source']['data'] = $base64Cod;		
		}
		else if($qualeAi == 'aiGemini') {
			unset($dataRic['contents']['parts'][0]['fileData']);	
			$dataRic['contents']['parts'][0]['inlineData']['data'] = $base64Cod;	
			$dataRic['contents']['parts'][0]['inlineData']['mimeType'] = $base64Media;	
		}
		else{
			$risAi['ok'] = false; 
			$risAi['error']['message'] = $modx->lexicon('mabolai.err_no_base64_permessa');
			$risAi = json_encode($risAi);
			header('Content-Type: application/json');
			echo $risAi;
			return;
		}	
	}
		
	//echo json_encode($dataRic);
	$ch = curl_init($parRic['apiLink']);
	curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
	curl_setopt($ch, CURLOPT_POST, true);
	curl_setopt($ch, CURLOPT_HTTPHEADER,$parRic['headers'] );
	if($qualeAi == 'aiStability'){
		//con 'file' => curl_file_create('/path/to/your/file.jpg', 'image/jpeg', 'filename.jpg') allego file
		curl_setopt($ch, CURLOPT_POSTFIELDS, $dataRic);
	}else{
		curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($dataRic));
	}

	$response = curl_exec($ch);
	$curlErrNum  = curl_errno($ch);
	$curlErrMsg = curl_error($ch);
	$httpStatus = curl_getinfo($ch, CURLINFO_HTTP_CODE);
	$contentType = curl_getinfo($ch, CURLINFO_CONTENT_TYPE);

	curl_close($ch);
	//echo $contentType;
	if ($curlErrNum) {
		$modx->log(MODX_LOG_LEVEL_ERROR, $modx->lexicon('mabolai.errore').": {$httpStatus} - {$curlErrMsg}");
		$risAi['ok'] = false; 
		$risAi['error']['message'] = $modx->lexicon('mabolai.errore').": {$httpStatus} - {$curlErrMsg}";
		$risAi = json_encode($risAi);
		header('Content-Type: application/json');
		echo $risAi;
		return;
	}
	//setto header 
	if (strpos($contentType, 'image') !== false || strpos($contentType, 'audio') !== false) {
		// File immagine , audio
		header('Content-Type: ' . $contentType);
		echo $response;
		return;
	} else if(strpos($contentType, 'application/json') !== false){	
		$result = json_decode($response, true);		
		//Creo la risposta
		$risAi = $modx->runSnippet('sGetAiRis', array('qualeAi' => $qualeAi,'tipoRic' => $tipoRic,'result' => $result,'miauscita' => $postInArr['dataRic']['miauscita']));			
		$risAi = json_encode($risAi);
		header('Content-Type: application/json');
		echo $risAi;
		//echo $response;
		//echo $result;
		//echo json_encode($parRic);
		//echo json_encode($dataRic);
		return;
	}else {		
		$modx->log(MODX_LOG_LEVEL_ERROR, $modx->lexicon('mabolai.err_contenttype') . '->' . $contentType);
		$risAi['ok'] = false; 
		$risAi['error']['message'] = $modx->lexicon('mabolai.err_contenttype');
		$risAi = json_encode($risAi);
		header('Content-Type: application/json');
		echo $risAi;
		return;
	}
}
//FINE inviaRicAi

The JS mabol.js
The js file contains all the functions, small or large that go to initiate/terminate the completion of MabolAi actions.
Many functions are simply show/hide while others make calls to the endpoint to get values.
Some in detail are in the topic dedicated to problems.

/* ==================================================================
VAR GLOBALI
================================================================== */
	//il converter di showdown
	var converter = new showdown.Converter();
	converter.setOption('noHeaderId', true);
	//converter.setOption('ghCodeBlocks', false);
/* ==================================================================
MABOL AI Funzioni utilizzate da Mabol AI
================================================================== */
MabolAI = {
	//Limita inserimento a numeri
	soloNum: function (event)
    {	
		allowedKeys = ['Backspace','1','2','3','4','5','6','7','8','9','0']

		if (!allowedKeys.includes(event.key)) {
			event.preventDefault();
			return false;
		} ;
    },
	//Limita inserimento a numeri e punto
	soloNumPunto: function (event)
    {	
		allowedKeys = ['Backspace','.','1','2','3','4','5','6','7','8','9','0']

		if (!allowedKeys.includes(event.key)) {
			event.preventDefault();
			return false;
		} ;
    },
	//Limita inserimento a numeri, punto e meno per i negativi
	soloNumPuntoMeno: function (event)
    {	
		allowedKeys = ['Backspace','.','1','2','3','4','5','6','7','8','9','0','-']

		if (!allowedKeys.includes(event.key)) {
			event.preventDefault();
			return false;
		} ;
    },
	//Crea src img da sola stringa base64
	getBase64Src: function (base64String)
    {	
		const binString = atob(base64String);
		const byteArray = Uint8Array.from(binString, (m) => m.codePointAt(0));
		
		const arr = (new Uint8Array(byteArray)).subarray(0, 4);
		let header = "";
		for (let i = 0; i < arr.length; i++) {
			header += arr[i].toString(16);
		}
		var base64Src = 0;
		switch (header) {
			case "89504e47":
				base64Src = "data:image/png;";
				break;
			case "47494638":
				base64Src = "data:image/gif;";
				break;
			case "ffd8ffe0":
			case "ffd8ffe1":
			case "ffd8ffe2":
			case "ffd8ffe3":
			case "ffd8ffe8":
				base64Src = "data:image/jpeg;";
				break;
			case "52494646": // RIFF
			if (byteArray[8] === 87 && byteArray[9] === 69 && byteArray[10] === 66 && byteArray[11] === 80) { // 'WEBP'
				base64Src = "data:image/webp;";
			}
			break
		}
		if(base64Src){
			base64Src += 'base64,'+base64String;
		}
		return base64Src;
    },
	//Apre messaggio informativo in base al tipo 1 ok, 2 errore
	apriMess: function (classe, frase1, frase2)
    {	
		const contMess = document.getElementById("mabolai-mess");
		//Lavoro per icona
		var icona = '';
		if(classe = 'messOk'){
			icona = '<i class="icon fa-check"></i>';
		}else{
			icona = '<i class="icon fa-times"></i>';
		}
		//la div col messaggio
		var htmlDiv  = '<div class="'+classe+'">';
			htmlDiv += '<div class="messIco">'+icona+'</div>';
			htmlDiv += '<div class="contTest">';
			htmlDiv += '<div class="messTest messBold">'+frase1+'</div>';
			htmlDiv += '<div class="messTest">'+frase2+'</div>';
			htmlDiv += '</div>';
			htmlDiv += '</div>';
		contMess.innerHTML += htmlDiv;
		contMess.style.display="flex";
		setTimeout(()=> contMess.style.display="none", 1000);
		setTimeout(()=> contMess.querySelector('.'+classe).remove(), 1100)
    },
    [...]
};
//fine MabolAI

When the generate button is clicked, the following function is called, which shunts, as appropriate, to other specifics for Ai and type of request.

//SMISTA LA Generazione dei Dati in base alla AI 
	getDataAI: function (tipoRic, att=false, obj=false)
    {	
		//Altre var
		const box = document.getElementById(tipoRic);
		const selAi = box.querySelector(".selAi").value;
		const boxAi = box.querySelector("."+selAi);
		const errPrompt = boxAi.querySelector(".errori_prompt");
		const errPara = boxAi.querySelector(".errori_para");
		//apro loader
		document.getElementById("mabolai-loader").style.display="block";
		//Cancello eventuali errori nella finestra
		boxAi.querySelector(".mabolai_errore").innerHTML = "";
		
		if(tipoRic != 'desc_img') {
			//svuoto ris generati e nascondo div dei risu
			boxAi.querySelector(".cont-risu-gen-data").innerHTML = "";
			boxAi.querySelector(".cont-risu").classList.add("chiusa");
		}
		//Reset errori su "input"
		boxAi.querySelectorAll('.mai-textarea').forEach(item => {
			item.classList.remove("errore");
		})
		if(tipoRic == 'gen_cont' || tipoRic == 'gen_campi' || tipoRic == 'mig_campi' || tipoRic == 'text_free' || tipoRic == 'trad_ris' || tipoRic == 'trad_free'){
			boxAi.querySelectorAll('.mai_para').forEach(item => {
				item.classList.remove("errore");
			})
			boxAi.querySelectorAll('.selRole').forEach(item => {
				item.classList.remove("errore");
			})
			boxAi.querySelectorAll('.selModel').forEach(item => {
				item.classList.remove("errore");
			})
			//Svuoto div errori
			errPara.innerHTML = "";
			errPara.style.display="none";
		}
		//Svuoto div errori
		errPrompt.innerHTML = "";
		errPrompt.style.display="none";
	
		//decido dove smistare
		if(selAi == 'aiOpen'){
			if(tipoRic == 'gen_cont' || tipoRic == 'gen_campi' || tipoRic == 'mig_campi' || tipoRic == 'text_free' || tipoRic == 'trad_ris' || tipoRic == 'trad_free'){
				MabolAI.getDataAITextOpen(boxAi, selAi, tipoRic);
			}
			if(tipoRic == 'gen_img'){
				MabolAI.getDataAIGenImgOpen(boxAi, selAi, tipoRic);
			}
			if(tipoRic == 'desc_img'){
				MabolAI.getDataAIDescImgOpen(boxAi, selAi, tipoRic, att, obj);
			}
			if(tipoRic == 'audio_ris' || tipoRic == 'audio_free'){
				MabolAI.getDataAIAudioOpen(boxAi, selAi, tipoRic);
			}
		}
		if(selAi == 'aiClaude'){
			if(tipoRic == 'gen_cont' || tipoRic == 'gen_campi' || tipoRic == 'mig_campi' || tipoRic == 'text_free' || tipoRic == 'trad_ris' || tipoRic == 'trad_free'){
				MabolAI.getDataAITextClaude(boxAi, selAi, tipoRic);
			}
			if(tipoRic == 'desc_img'){
				MabolAI.getDataAIDescImgClaude(boxAi, selAi, tipoRic, att, obj);
			}
		}
		if(selAi == 'aiMistral'){
			if(tipoRic == 'gen_cont' || tipoRic == 'gen_campi' || tipoRic == 'mig_campi' || tipoRic == 'text_free' || tipoRic == 'trad_ris' || tipoRic == 'trad_free'){
				MabolAI.getDataAITextMistral(boxAi, selAi, tipoRic);
			}
			if(tipoRic == 'desc_img'){
				MabolAI.getDataAIDescImgMistral(boxAi, selAi, tipoRic, att, obj);
			}
		}
		if(selAi == 'aiGemini'){
			if(tipoRic == 'gen_cont' || tipoRic == 'gen_campi' || tipoRic == 'mig_campi' || tipoRic == 'text_free' || tipoRic == 'trad_ris' || tipoRic == 'trad_free'){
				MabolAI.getDataAITextGemini(boxAi, selAi, tipoRic);
			}
			if(tipoRic == 'gen_img'){
				MabolAI.getDataAIGenImgGemini(boxAi, selAi, tipoRic);
			}
			if(tipoRic == 'desc_img'){
				MabolAI.getDataAIDescImgGemini(boxAi, selAi, tipoRic, att, obj);
			}
		}
		if(selAi == 'aiDeepl'){
			if(tipoRic == 'mig_campi' || tipoRic == 'trad_ris' || tipoRic == 'trad_free'){
				MabolAI.getDataAITextDeepl(boxAi, selAi, tipoRic);
			}
		}
		if(selAi == 'aiGetimg'){
			if(tipoRic == 'gen_img'){
				MabolAI.getDataAIGenImgGetimg(boxAi, selAi, tipoRic);
			}
		}
		if(selAi == 'aiStability'){
			if(tipoRic == 'gen_img'){
				MabolAI.getDataAIGenImgStability(boxAi, selAi, tipoRic);
			}
		}
		if(selAi == 'aiEleven'){
			if(tipoRic == 'audio_ris' || tipoRic == 'audio_free'){
				MabolAI.getDataAIAudioEleven(boxAi, selAi, tipoRic);
			}
		}
	},

The following is the one specific to openAi that has text as the answer.

//OPEN AI - OPEN AI - OPEN AI - OPEN AI - OPEN AI - OPEN AI - OPEN AI - OPEN AI 	
	getDataAITextOpen: function (boxAi, qualeAi, tipoRic)
    {	
		//Loader aperto, errori resettati, risultati chiusi
		
		//Altre var
		const errPrompt = boxAi.querySelector(".errori_prompt");
		const errPara = boxAi.querySelector(".errori_para");
		
		var errore = false;
					
		//Parametri senza controllo di errore, eventualmente ritorna errore da API Open AI
		const modelLink = boxAi.querySelector(".selService").value;
		const model = boxAi.querySelector(".selModel").value;
		var store = boxAi.querySelector(".selStore").value;
		if(store == 'false'){
			store = false;
		}else{
			store = true;
		}
		//Parametri numerici senza controllo di errore, vanno lavorati a seconda dei casi
		var tokens = parseInt(boxAi.querySelector(".para_tokens").innerText.trim());
		var temp = parseFloat(boxAi.querySelector(".para_temp").innerText.trim());
		if(temp < 0){temp = 0} 
		if(temp > 2){temp = 2}
		var p = parseFloat(boxAi.querySelector(".para_p").innerText.trim());
		if(p < 0){p = 0} 
		if(p > 1){p = 1}
		var n = parseInt(boxAi.querySelector(".para_n").innerText.trim());
		if(n > 10){n = 10}
		var frequency_penalty = parseFloat(boxAi.querySelector(".para_frequency_penalty").innerText.trim());
		if(frequency_penalty < -2){frequency_penalty = -2} 
		if(frequency_penalty > 2){frequency_penalty = 2}
		var presence_penalty = parseFloat(boxAi.querySelector(".para_presence_penalty").innerText.trim());
		if(presence_penalty < -2){presence_penalty = -2} 
		if(presence_penalty > 2){presence_penalty = 2}
		
		//Creo il prompt 
		//PROMPT !
		if(tipoRic == 'trad_ris' || tipoRic == 'trad_free'){
			var usaSelLang = boxAi.querySelector(".checkSelLangTrad");
			const promptPre = boxAi.querySelector(".prompt_pre");
			const promptPreFissa = boxAi.querySelector(".prompt_pre_fissa");
			if(usaSelLang.checked == true){
				var prompt1 = promptPreFissa.innerText.trim();
			}else{
				var prompt1 = promptPre.innerText.trim();	
			}
			prompt1 += boxAi.querySelector(".prompt_ini").innerText.trim();
		}else{
			var prompt1 = boxAi.querySelector(".prompt_ini").innerText.trim();
		}
		//PROMPT 2
		if(tipoRic == 'text_free'){
			var prompt2 = boxAi.querySelector(".prompt_fine").innerText.trim();
			var prompt2PerErr = boxAi.querySelector(".prompt_fine").innerText.trim();
		}else if(tipoRic == 'trad_free'){
			var prompt2 = boxAi.querySelector(".prompt_fine").innerHTML.trim();
			var prompt2PerErr = boxAi.querySelector(".prompt_fine").innerText.trim();
		}else{
			var prompt2 = boxAi.querySelector(".prompt_dati").innerHTML.trim();
			prompt2 += boxAi.querySelector(".prompt_fine").innerText.trim();
			var prompt2PerErr = boxAi.querySelector(".prompt_dati").innerText.trim()+boxAi.querySelector(".prompt_fine").innerText.trim();
		}
		var promptAi = prompt1 + prompt2		
		var promptAiError = prompt1 + prompt2PerErr
		
		//Prendo i role 
		const selRole1 = parseInt(boxAi.querySelector(".selRole1").value);
		const selRole2 = parseInt(boxAi.querySelector(".selRole2").value);		
		var role1 = '';
		var role2 = '';
		switch(selRole1) {
			case 1:
				role1 = 'developer';
				break;
			case 2:
				role1 = 'user';
				break;
		}
		if(selRole2){role2 = 'user';}
				
		//Errore stessi linguaggi in trad_ris e trad_free
		if(tipoRic == 'trad_ris' || tipoRic == 'trad_free'){
			var usaSelLang = boxAi.querySelector(".checkSelLangTrad");
			if(usaSelLang.checked == true){
				const langDa = boxAi.querySelector(".selLangTradDa").value;
				const langA = boxAi.querySelector(".selLangTradA").value;
				if(langDa == langA){
					errore = true;
					errPrompt.innerHTML += '<div class="singErr">'+_("mabolai.err_stessa_lingua")+'</div>';
				}
			}
		}
		//Errore prompt totale
		if (!promptAiError){
			errore = true;
			errPrompt.innerHTML += '<div class="singErr">'+_("mabolai.err_no_prompt")+'</div>';
			boxAi.querySelectorAll('.mai-textarea').forEach(item => {
				item.classList.add("errore");
			})
		}
		//Errore Ruoli, già lo fa onchange sulla select ma ridondiamo. Ruolo 1 sempre presente, la select impone un valore
		//In pratica ruolo 2 non deve mai essere uguale o minore di ruolo 1, se esiste ruolo 2
		if (selRole2 && selRole2 <= selRole1 ){ 
			errore = true;
			errPara.innerHTML += '<div class="singErr">'+_("mabolai.err_role")+'</div>';
			boxAi.querySelector(".selRole1").classList.add("errore");
			boxAi.querySelector(".selRole2").classList.add("errore");
		}
		//se role2 c'è anche role 1 e si procede con due role, else esiste solo role 1 che è fissato dalla select
		if(role2){
			//controllo che esista il prompt 1 e prompt 2, se tutto vuoto già controllato	
			if(!prompt1){				
				errore = true;
				errPrompt.innerHTML += '<div class="singErr">'+_("mabolai.err_no_prompt1")+'</div>';
				boxAi.querySelector(".prompt_ini").classList.add("errore");
			}
			if(!prompt2PerErr){				
				errore = true;
				errPrompt.innerHTML += '<div class="singErr">'+_("mabolai.err_no_prompt2")+'</div>';
				boxAi.querySelector(".prompt_fine").classList.add("errore");
			}
			//Creo oggetto data da inviare alla AI, con 2 role e 2 prompt 	
			if(modelLink == 'responses'){
				var data = {
					model: model,
					store: store,
					text: {format: {type: "text"}},
					instructions: prompt1,
					input: prompt2,
					max_output_tokens: tokens,
					temperature: temp,
					top_p: p,
					modelLink: modelLink,
					miauscita: modelLink
				};
			}else{
				var data = {
					model: model,
					store: store,
					response_format: { "type": "text" },
					messages: [{role: role1, content: prompt1},{role: role2, content: prompt2}],
					max_completion_tokens: tokens,
					temperature: temp,
					top_p: p,
					frequency_penalty: frequency_penalty,
					presence_penalty: presence_penalty,
					n: n,
					modelLink: modelLink
				};
			}		
		}else{
			//Un solo prompt già controllato
			//Creo oggetto data da inviare alla AI, un solo ruolo e tutto il prompt
			if(modelLink == 'responses'){
				var data = {
					model: model,
					store: store,
					text: {format: {type: "text"}},
					input: [{role: role1, content: promptAi}],
					max_output_tokens: tokens,
					temperature: temp,
					top_p: p,
					modelLink: modelLink,
					miauscita: modelLink
				};
			}else{
				var data = {
					model: model,
					store: store,
					response_format: { "type": "text" },
					messages: [{role: role1, content: promptAi}],
					max_completion_tokens: tokens,
					temperature: temp,
					top_p: p,
					frequency_penalty: frequency_penalty,
					presence_penalty: presence_penalty,
					n: n,
					modelLink: modelLink
				};
			}			
		}
		// se errori presenti ritorno false e mostro div errori
		if (errore) {		
			errPrompt.style.display="flex";
			errPara.style.display="flex";
			//Chiudo loader e ritorno
			document.getElementById("mabolai-loader").style.display="none";
			document.querySelector(".mabolai-actions-box").scrollIntoView({ behavior: "smooth", block: "end", inline: "nearest" });
			return false;
		}
		MabolAI.inviaAiRic(boxAi, qualeAi, tipoRic, data);
	},

All AI and request type specific functions (genimg, gencont etc etc) send to the function calling endpoint.

//INVIO RICHIESTA - INVIO RICHIESTA - INVIO RICHIESTA - INVIO RICHIESTA - INVIO RICHIESTA - INVIO RICHIESTA - INVIO RICHIESTA - INVIO RICHIESTA - INVIO RICHIESTA
	//Vale per tutti
	async inviaAiRic(boxAi, qualeAi, tipoRic, data, att=false, obj=false) 
	{	
		var inizioT = new Date().getTime();
		//cancello eventuale input rimasto in precedenza
		const jsFileTemp = document.getElementById("jsFileTemp");
		if(jsFileTemp){jsFileTemp.remove();}
		const tab = document.querySelector(".mabolai-actions-box");
		const loader = document.getElementById("mabolai-loader");
		const contErr = boxAi.querySelector(".mabolai_errore");
		//Gestione errori mio endpoint
		const erroreGest = async (response) => {
            if (!response.ok) {
                const ris = await response.json();
                if (ris?.error) {
					loader.style.display="none";
					contErr.innerHTML = "<pre>" + ris.error.message + "</pre>";
					tab.scrollIntoView({ behavior: "smooth", block: "end", inline: "nearest" });
                    throw new Error(ris.error.message);
                }
				loader.style.display="none";
				contErr.innerHTML = `${response.status} ${response.statusText}`;
				tab.scrollIntoView({ behavior: "smooth", block: "end", inline: "nearest" });
                throw new Error(`${response.status} ${response.statusText}`);
            }
        }
		//la fetch che rimanda al mio endpoint
		const response = await fetch(MODx.config.mabol_url_fun_mabolai + 'mabolAiFun.php', {  			
				method: "POST", 
				headers: { 
					"Content-Type": "application/json"
				},
				body: JSON.stringify({
						funzione: 'inviaRicAi',
						qualeAi: qualeAi,
						tipoRic: tipoRic,
						dataRic:data
						}) 
            }); 
		//Errori dal mio endpoint
		await erroreGest(response);
		const contentType = response.headers.get('content-type');
		if(contentType && contentType.includes('application/json')){
			var ris = await response.json();
		}else{
			//un file
			const blob = await response.blob();
			const url = URL.createObjectURL(blob);
			const fileSize = blob.size;
			// costruisco ris che viene poi passato
			const fineT = new Date().getTime();
            const tempo = ((fineT - inizioT) / 1000).toFixed(3);
			const info = '<div class="ai_tempo">'+_("mabolai.ai_tempo")+' <span>'+tempo+'</span> '+_("mabolai.ai_sec")+'</div>';
			var ris = {ok: true, data:[url], info:info, fileSize:fileSize};
			
			//Creazione file temporaneo da prelevare in seguito
			const file = new File([blob], "file_scaricato", { type: blob.type });
			const dataTransfer = new DataTransfer();
			dataTransfer.items.add(file);
			//creo input file dove inserire file
			const fileInput = document.createElement('input');
			fileInput.type = 'file';
			fileInput.id = 'jsFileTemp';
			fileInput.style.display = 'none'; 
			boxAi.appendChild(fileInput);
			fileInput.files = dataTransfer.files;
		}
		
		//Errori dalla chiamata
		if (!ris.ok) {
			if (ris?.error) {
				loader.style.display="none";
				contErr.innerHTML = "<pre>" + ris.error.message + "</pre>";
				tab.scrollIntoView({ behavior: "smooth", block: "end", inline: "nearest" });
				throw new Error(ris.error.message);
			}
		}		
		//lavoro con ris e ritorno a seconda dei casi
		if(tipoRic == 'gen_cont' || tipoRic == 'gen_campi' || tipoRic == 'mig_campi' || tipoRic == 'text_free' || tipoRic == 'trad_ris' || tipoRic == 'trad_free'){
			MabolAI.rispostaAIText(boxAi, qualeAi, tipoRic, ris);
		}
		if(tipoRic == 'gen_img'){
			MabolAI.rispostaAIGenImg(boxAi, qualeAi, tipoRic, ris);
		}
		if(tipoRic == 'desc_img'){
			MabolAI.rispostaAIDescImg(boxAi, qualeAi, tipoRic, ris, att, obj);
		}
		if(tipoRic == 'audio_ris' || tipoRic == 'audio_free'){
			MabolAI.rispostaAIAudio(boxAi, qualeAi, tipoRic, ris);
		}
	},

As you can see depending on the case it uses certain parameters, or makes call with formdata or json.
All of these functions started from something simpler that then expanded so maybe in addition to being improvable, they contain elements that might not seem to make sense.
The send request function then sorts to show the result to other functions that depend ONLY on the type of request (response) and are independent of the AI type, even if the parameter is sent.
Next is the one that creates the response for an image generation

//RISPOSTA GEN IMG - RISPOSTA GEN IMG - RISPOSTA GEN IMG - RISPOSTA GEN IMG - 
	rispostaAIGenImg: function (boxAi, qualeAi, tipoRic, ris)
    {	 
		//Costruiamo il risultato almeno 1 sempre presente
		const box = document.getElementById(tipoRic);
		const divGen = boxAi.querySelector(".cont-risu-gen-data");
		const divInfo = boxAi.querySelector(".cont-ai");
		const tab = document.querySelector(".mabolai-actions-box");
		const loader = document.getElementById("mabolai-loader");
		const contErr = boxAi.querySelector(".mabolai_errore");
		
		//Prendo dimensione immagine e tipo di uscita ed eventuale popup
		var dimImg = 0;
		var tipoUscita = 0;
		var popUp = 0;
		var titPopUp = '';
		if(qualeAi == 'aiOpen') {
			const selTipoUscita = boxAi.querySelector(".selFormRes");			
			const selModel = boxAi.querySelector(".selModel");
			tipoUscita = selTipoUscita.value;
			var titoloF = _("mabolai.dim_img");
			if(selModel.value == 'dall-e-2'){
				dimImg = boxAi.querySelector(".selSize2").value;
			}else{
				dimImg = boxAi.querySelector(".selSize3").value;
			}
			if(dimImg != '256x256' && dimImg != '512x512'){
				popUp = 1;
				titPopUp = titoloF+' '+dimImg;
			}
		}
		if(qualeAi == 'aiGetimg') {
			const selTipoUscita = boxAi.querySelector(".selFormRes");			
			const selModel = boxAi.querySelector(".selModel");
			tipoUscita = selTipoUscita.value;
			if(selModel.value == 'essential-v2'){
				dimImg = boxAi.querySelector(".selaspect_ratio").value;
				var titoloF = _("mabolai.rapporto_img");
			}else{
				var width = boxAi.querySelector(".para_width").innerText.trim();
				var height = boxAi.querySelector(".para_height").innerText.trim();
				dimImg = width+'x'+height;
				var titoloF = _("mabolai.dim_img");
			}
			if(dimImg != '256x256' && dimImg != '512x512'){
				popUp = 1;
				titPopUp = titoloF+' '+dimImg;
			}
		}
		if(qualeAi == 'aiStability') {
			const selTipoUscita = boxAi.querySelector(".selFormRes");	
			tipoUscita = selTipoUscita.value;
			var titoloF = _("mabolai.rapporto_img");
			dimImg = boxAi.querySelector(".selaspect_ratio").value;
			popUp = 1;
			titPopUp = titoloF+' '+dimImg;
		}
		if(qualeAi == 'aiGemini') {
			tipoUscita = 'b64';
			var titoloF = _("mabolai.rapporto_img");
			const asr = boxAi.querySelector(".selaspect_ratio");
			if(asr){
				dimImg = asr.value;
			}else{
				dimImg = '1:1';
			}
			popUp = 1;
			titPopUp = titoloF+' '+dimImg;
		}
		
		//vedo eventuali tv da settare da settare
		var setTV = 0;		
		const divSetTV = box.querySelector(".contSetTV");
		if(divSetTV){
			setTV = 1;
			var contSetTV = divSetTV.innerHTML.trim();			
		}
		
		//Ciclo sui risultati, le immagini
		for (i=0; i<ris.data.length; i++) { 		
			//vedo tipo uscita		
			if( tipoUscita == 'b64_json' || tipoUscita == 'b64') {
				//routine per cercare MimeType, guardo che non sia presente
				const mime = ris.data[i].trim().slice(0, 4);
				if(mime == 'data'){
					var imgSrc = ris.data[i].trim();
				}else{
					var res = MabolAI.getBase64Src(ris.data[i].trim());
					if (res){
						var imgSrc = res;
					}else{
						loader.style.display="none";
						contErr.innerHTML = "<pre>"+_("mabolai.err_formato_uscita")+"</pre>";
						tab.scrollIntoView({ behavior: "smooth", block: "end", inline: "nearest" });
						return true;
					}
				}
				
			}else if( tipoUscita == 'url' || tipoUscita == 'image') {
				var imgSrc = ris.data[i].trim();				
			}else{
				loader.style.display="none";
				contErr.innerHTML = "<pre>"+_("mabolai.err_formato_uscita")+"</pre>";
				tab.scrollIntoView({ behavior: "smooth", block: "end", inline: "nearest" });
				return true;
			}			
			var htmlDiv  = '<div class="sing-gen">';
				htmlDiv += '<div class="sing-gen-data"><img src="'+ imgSrc +'"></div>';
				htmlDiv += '<div class="sing-gen-cont-desc">';
				//Se c'è dimensione, praticamentre sempre 
				if(dimImg){
					//Se c'è popUp
					if(popUp){
						htmlDiv += '<button class="sing-gen-popup" type="button" onclick="MabolAI.apriPopUp(this,\''+tipoRic+'\',\''+titPopUp+'\',\'src\',\''+imgSrc+'\')"><i class="icon fa-search-plus"></i></button>';
					}
					htmlDiv += '<div class="sing-gen-desc">'+titoloF+' <span>'+dimImg+'</span></div>';
				}
				htmlDiv += '<div class="contSettaMulti">';
				//se base 64 copio altrimenti salvo e nel caso aggiungo setTv
				if(tipoUscita == 'b64_json' || tipoUscita == 'b64'){
					htmlDiv += '<div class="sing-gen-copia"><div class="nomeTV">'+ _("mabolai.copia_src_base64") +'</div><button type="button" onclick="MabolAI.copiaBase64(this)"><i class="icon icon-files-o"></i></button></div>';
				}
					htmlDiv += '<div class="sing-gen-copia"><div class="nomeTV">'+ _("mabolai.salva_img") +'<span>'+ _("mabolai.salva_img_percorso") +'</span></div><button type="button" onclick="MabolAI.salvaFile(this,\'img\')"><i class="icon fa-save"></i></button></div>';
					//Se SetTv inserisco div coi set di TV
					if(setTV){
						htmlDiv += contSetTV;
					}	
				htmlDiv += '</div></div></div>';
			divGen.innerHTML += htmlDiv;	
			if( tipoUscita == 'image') {
				//URL.revokeObjectURL(imgSrc);				
			}		
		}
		divInfo.innerHTML = ris.info;
		
		//Mostro e ritorno
		boxAi.querySelector(".cont-risu").classList.remove("chiusa");
		loader.style.display="none";
		tab.scrollIntoView({ behavior: "smooth", block: "end", inline: "nearest" });
		
		return true;
	},	

ISSUE
All the code and explanation part done above is just to make you understand how it works and is introductory to the error part that follows
Already from the code you have seen you get an idea of the messes I have created for myself and that better solutions just the order of the day.
Having said that I have come to the highlight of the topic with the errors seeking solution, with the understanding that improvements are always super welcome

Problem no Problem
The loader and the action confirmation block, are not the ones native to Modx (I didn’t know how to trigger them) but I built them similarly and you can’t see much difference, I think. As you can see from the initial video
MabolAi, of course, also handles errors, both internal and those that possibly come from AI.
In various tests, I agreed that if, having gotten the error ( or even a simple responsive), I scrolled with js to the div to be displayed there was a problem.
Basically in scrollIntoView({ behavior: “smooth”, block: “end”, inline: “nearest” }); as block parameter I could only use start or end not center or nearest.


In the image you can see that it creates the same problem, which is known, as when you open the help page in the manager (up to 3.1.1 I have not yet tried 3.1.2)

Plugin Event and Save Button
As you’ve seen the plugin only triggers on the OnDocFormRender event, is that okay? is that enough for how it works?( if I’ve managed to make it clear)
Not directly related to the event there is override of the save button of the resource.
As avet seen at the beginning in the plugin I did override the save button

Ext.override(MODx.panel.Resource, {
            originalSuccess: MODx.panel.Resource.prototype.success
            , success: function (o) {
                this.originalSuccess(o);
				var ricarica = ' . $resource->get('id') . ';
				if (ricarica) {
					var url = location.href, i = url.indexOf("?") + 3;
					MODx.loadPage(url.substr(i));
				}
            }
        });

I need this because when I set a resource field, it does not reload the page and all the elements taken from MabolAi are not updated.
I put as a trigger parameter existence of resource id, otherwise it would reload and return to a resource creation when I save in resource creation,
Is this okay as a solution? does it create internal problems in MODx (I don’t think so)? Is there a better solution maybe related to the events where the plugin is triggered?

Manager Change Lexicon
As I wrote in the description of operation, the AI-specific blocks, are generated from a snippet.
That snippet is called either directly when opening the MabolAi Tab, or (onchange) from a function in the internal endpoint.
In this case, I noticed that the language phrases generated in this way were not loading, or rather not always.
If I only changed the language from the manager menu they would not load, if in addition to this change I also put system setting cultureKey to the same value instead they would load.
In other words, if the manager language matched the value of the cultureKey setting, the language phrases loaded from snippet launched in endpoint would show up, otherwise they would not.
I tried to figure it out but without any success.
Do you have any idea what should be done to keep the problem from showing up? also because culturKey, by its description, refers only to not-mgr contexts

New MODx 3.x Nomenclature
The nomenclature I used in the various snippets as the following:

use MODX\Revolution\modTemplateVarTemplate;
$qTV = $modx->newQuery(modTemplateVarTemplate::class);

Is it ok? will be the one that will always be used in the future for MODx >3.0.0?
Because although I have the empty error log, I have many errors in the deprecated logs, such as the following:
modTemplateVarTemplate Deprecato dal: ** v3.0 | **Replace references to class modTemplateVarTemplate with MODX\Revolution\modTemplateVarTemplate to take advantage of PSR-4 autoloading.

Trigger Alert Exit Page
After several checks, I don’t see any problems, but is there a trigger that starts alerts when exiting the page after making changes?
That is, when I set a field and exit the page without saving, do I need to set some value to make sure it starts alerts that warn that there are changes being made?
I saw that however even modx sometimes activates it and sometimes not

Input hidden-content
To take the current value of the resource content in js, I used the value of the hidden input with id = hidden-content, which I saw was present in the manager template when editing/creating a resource.
It turned out to be much more convenient and immediate.
Is this input a “standard” in MODx and will it be maintained? Or am I already moving to use something else?

addPackage “outside”
In the function file that is used as an endpoint, after initializing MODx I had to load the Gallery package, otherwise the values in its DB tables would not be taken.

require_once dirname(__FILE__,3) . '/config.core.php';
require_once MODX_CORE_PATH . 'model/modx/modx.class.php';
$modx = new modX();
$modx->initialize('mgr');

//Caricare tabelle personalizzate componenti che utilizzo
$modx->addPackage('Gallery', MODX_CORE_PATH.'components/gallery/model/');
...
$oItem = $modx->newObject('galItem');

Is this okay as a solution or is there something better considering the operation used?

Set Image Field
When the Ai generated a value I had to set the relevant resource or TV field, so that then saving the resource would update and store everything.
A problem, although not blocking, that has stuck me for quite some time and I still haven’t found an optimal solution.
It concerns TVs of type image, imageplus and galleryitem, when I have to set their value and get everything working including the preview in the TV tab.
For text values, both resource fields and tv I had no problems.

}else if(idCampo == 'tiny'){
			//tiny sia per tinyMce che per tinyMCE RTE devo prendere dentro iframe
			var iframe = document.getElementById("ta_ifr");
			var campo = iframe.contentWindow.document.getElementById('tinymce');
			campo.innerHTML = messVal;
		}else if(idCampo == 'seosuite-longtitle'){
			//Con seosuite devo modificare anche il campo originale
			var campo = document.getElementById(idCampo);
			campo.value = messVal;
			var campoOri = document.getElementById('modx-resource-longtitle');
			campoOri.value = messVal;
		}else if(idCampo == 'seosuite-description'){
			//Con seosuite devo modificare anche il campo originale
			var campo = document.getElementById(idCampo);
			campo.value = messVal;
			var campoOri = document.getElementById('modx-resource-description');
			campoOri.value = messVal;
		}else{
			var campo = document.getElementById(idCampo);
			campo.value = messVal;
		}
		divMess.classList.add("settato");
        setTimeout(()=> divMess.classList.remove("settato"), 500);
		MabolAI.apriMess('messOk', _("mabolai.successo"), _("mabolai.settato_successo"));
		//MODx.fireResourceFormChange();

Above MessVal is the value I had to give to the field, all is well, in case of SeoSuite I had to set also the hidden field otherwise it didn’t take the change by saving.
In case of a richText I was passing from iframe.
Coming to the images, to my knowledge, I went crazy.
Common problem is that in creation certain inputs were not present.
Regardless of the type I had available the path to the image which in case of source was just the name (with a source >1)

const ris = await MabolAI.salvaFile(obj, tipoFile, source, tvTipo, true);
if(source >1) {
			var pathFile = ris.nomeFile;
		}else{				
			var pathFile = ris.relPath;
		}

Below is the js code to set the values according to the 3 types of image.
The following is triggered when the AI-obtained image setting button is clicked

if(tvTipo == 'image'){
			const tv = document.getElementById('tv'+tvId);
			const tvBro = document.getElementById('tvbrowser'+tvId);
			tv.value = pathFile;
			const focusEvent = new Event('focus', {bubbles: true,cancelable: true});
            tvBro.dispatchEvent(focusEvent);
			tvBro.value = pathFile;
			const blurEvent = new Event('blur', {bubbles: true,cancelable: true});
            tvBro.dispatchEvent(blurEvent);
			/*const tvPre = document.getElementById('tv-image-preview-'+tvId).querySelector("img");
			var newF = pathFile.split('.').pop();
			var url = new URL(tvPre.src, MODx.config.site_url);
			var params = new URLSearchParams(url.search);
			params.set('src', pathFile);
			params.set('f', newF);
			params.set('source', source);
			url.search = params.toString();
			var newSrc = url.pathname + url.search;
			tvPre.src = newSrc;*/
		}
		if(tvTipo == 'imageplus'){
			//inDB = true;
			const tvPlus = document.getElementById('imageplus-panel-input-div-'+tvId).querySelector("input");	
			const focusEvent = new Event('focus', {bubbles: true,cancelable: true});
            tvPlus.dispatchEvent(focusEvent);	
			tvPlus.value = pathFile;		
			const blurEvent = new Event('blur', {bubbles: true,cancelable: true});
            tvPlus.dispatchEvent(blurEvent);
			const tvPlusTrig = tvPlus.parentElement.parentElement.parentElement;
			tvPlusTrig.blur();
			//MODx.fireResourceFormChange();		
		}
		if(tvTipo == 'galleryitem'){
			//inDB = true;
			const tvGal = document.getElementById('tv'+tvId);	
			var tvGalPre = document.getElementById('tv'+tvId+'-image');
			const tv_gal_id = document.getElementById('tv'+tvId+'-gal_id');
			const tv_gal_album = document.getElementById('tv'+tvId+'-gal_album');
			const tv_gal_src = document.getElementById('tv'+tvId+'-gal_src');
			const tv_gal_orig_width = document.getElementById('tv'+tvId+'-gal_orig_width');
			const tv_gal_orig_height = document.getElementById('tv'+tvId+'-gal_orig_height');
			const tv_gal_name = document.getElementById('tv'+tvId+'-gal_name');
			const tv_gal_image_width = document.getElementById('tv'+tvId+'-gal_image_width');
			const tv_gal_image_height = document.getElementById('tv'+tvId+'-gal_image_height');
			tvGal.value = ris.gallData;
			const galDataJson = JSON.parse(ris.gallData);
			if(!tvGalPre){
				const tvGalPreDiv = document.getElementById('tv'+tvId+'-preview');
				const srcImg = '/assets/components/gallery/connector.php?action=web/phpthumb&src='+galDataJson.pathImg+'&h='+galDataJson.gal_image_height+'&w='+galDataJson.gal_image_width+'&zc='+MODx.config.phpthumb_zoomcrop+'&amp;far='+MODx.config.phpthumb_far+'&fltr[]=rot|0&'
				const divPreImg = '<img id="tv'+tvId+'-image" class="" src="'+srcImg+'" alt="'+galDataJson.gal_name+'"   style="width: '+galDataJson.gal_image_width+'px; height: '+galDataJson.gal_image_height+'px">';
				tvGalPreDiv.innerHTML = divPreImg;
			}else{
				var url = new URL(tvGalPre.src, MODx.config.site_url);
				var params = new URLSearchParams(url.search);
				params.set('src', galDataJson.pathImg);
				params.set('w', galDataJson.gal_image_width);
				params.set('h', galDataJson.gal_image_height);
				url.search = params.toString();
				var newSrc = url.pathname + url.search;
				tvGalPre.src = newSrc;
			}
			tv_gal_id.value = galDataJson.gal_id;
			tv_gal_album.value = galDataJson.gal_album;
			tv_gal_src.value = galDataJson.gal_src;
			tv_gal_orig_width.value = galDataJson.gal_orig_width;
			tv_gal_orig_height.value = galDataJson.gal_orig_height;
			tv_gal_name.value = galDataJson.gal_name;
			tv_gal_image_width.value = galDataJson.gal_image_width;
			tv_gal_image_height.value = galDataJson.gal_image_height;			
			//MODx.fireResourceFormChange();
		}	
		if(tvTipo == 'fileaudio'){
			//inDB = true;
			const tv = document.getElementById('tv'+tvId);	
			const tvBro = document.getElementById('tvbrowser'+tvId);
			tv.value = pathFile;	
			tvBro.value = pathFile;	
		}

Initially I thought of going directly from saving the TV in the db and then having the page reloaded, this didn’t create any problems, it worked but it was definitely unattractive and didn’t allow working on any crops or actions with imageplus or galleryitem.

With image you have to update the value of the hidden input tvX, in the visible input tvbrowserX and src of the preview image inside the div tv-image-preview-X.
The first two values gave me no problems, while for preview I had problems.
Initially, the commented part /* */, I used to pass to aggiornanre src of the imagin, it worked but not convenient, eventually I used the method that I later used also in imageplus.
In the TV Tab I saw that by changing the value of the visible input (tvbrowserX), without going from broswerfile and then clicking externally everything was updated.
So I did, setting from the MabolAi tab, and also used for imageplus, as you can see in the code.
The problem is that extra click that I can’t get triggered without doing it right manually.
As you can see in the video, with more emphasis for imageplus which triggers crop window, once image is set I have to click anywhere for preview to take effect.
Actually a click necessarily happens any way, either by going back to the Tv tab or by saving resource, so there are no practical problems.
The thing is annoying, though.
I should be able to get that click ceh trigger the preview, in imageplus also crop window, on just the click of the set button and not on a second click.
I have tried them all but with no solution.
If anyone has any ideas I’m all ears. :ear:
Ultimately by clicking set button under image obtained I have to change input of TV, see the preview and make sure that by saving everything is updated

With galleryitem instead the problem that when creating the resource I have to create input that turns out not to be there and some times going to the TVs page the first action (crop, resize) does not happen.

That’s all!
I hope I have given an idea of how it works, and I hope someone knows how to solve the problems encountered.
Needless to reiterate that all possible improvements are welcome.
All opinions and negative comments heard.

Thanks in advance and I hope I don’t click something wrong like a few days ago :man_facepalming: :grimacing: :grinning:

Wow! Thanks for all of the work you did on this. :clap:

1 Like

Wow! This looks amazing. I will take some time over the next week to dig into this in a bit more detail. Fantastic work @SilverMabol !

1 Like