/
home
/
rekodeb
/
photobooth
/
wp-content
/
plugins
/
otomatic-ai
/
app
/
Modules
/
Upload File
HOME
<?php namespace OtomaticAi\Modules; use DOMDocument; use DOMXPath; use Exception; use OtomaticAi\Models\Contracts\Publishable; use OtomaticAi\Modules\Contracts\Module as ModuleContract; use OtomaticAi\Models\Presets\Preset; use OtomaticAi\Vendors\Illuminate\Support\Arr; use OtomaticAi\Vendors\Illuminate\Support\Str; use OtomaticAi\Utils\ContentData; use OtomaticAi\Utils\DataSync; use OtomaticAi\Utils\Image; use OtomaticAi\Utils\PresetCollection; use OtomaticAi\Utils\ProjectSettings\ProjectSettingsFactory; use OtomaticAi\Utils\Settings; use OtomaticAi\Utils\Support; use OtomaticAi\Vendors\GuzzleHttp\Client; use OtomaticAi\Vendors\Illuminate\Support\Collection; class ProcessTextModule extends Module implements ModuleContract { const SLUG_NAME = "process_text_module"; /** * Execute the job. * * @return void * @throws Exception */ public function handle(): void { // verify that the job is runnable if (!$this->isRunnable()) { return; } // log the start of the job $this->publication->addLog("Text module started.", self::SLUG_NAME); if ($this->getModuleValue("content.automatic.enabled", false)) { // create the automatic template $this->createAutomaticTemplate(); // add missing custom links $this->addMissingCustomLinks(); } else { // generate with the blocks $this->generateWithBlocks(); } // add source link if enabled for news type if (($this->getType() === "news" || $this->getType() === "news-now") && $this->getModuleValue("content.sources.enabled", false)) { $url = Arr::get($this->publication->meta, 'url'); $host = parse_url($url, PHP_URL_HOST); if (!empty($host)) { $link = $this->html->createElement("a"); $link->setAttribute("href", $url); $link->setAttribute("rel", $this->getModuleValue("content.sources.no_follow", false) ? "nofollow" : ""); $link->setAttribute("target", $this->getModuleValue("content.sources.blank", false) ? "_blank" : ""); $link->nodeValue = $host; $el = $this->html->createElement("p"); $el->appendChild($this->html->createTextNode("Source: ")); $el->appendChild($link); $this->html->getElementsByTagName("body")->item(0)->appendChild($el); } } // add the html content to the generation state $this->generationState->setArtifact("html_content", trim(preg_replace("/^<body>|<\/body>$/", "", $this->html->saveHTML($this->html->getElementsByTagName("body")->item(0))))); // log the end of the job $this->publication->addLog("Text module completed successfully.", self::SLUG_NAME, "success"); } /** * Create the automatic template * * @return void */ private function createAutomaticTemplate(): void { // make data for the preset $data = $this->makePayloadData(); // get preset $preset = Preset::findFromAPI("generate_automatic_content", $this->getModuleValue("models.text", "gpt-4o-mini")); // transform preset with the correct model $attributes = $preset->toArray(); Arr::set($attributes, "model", $this->getModuleValue("models.text", "gpt-4o-mini")); $preset = Preset::make($attributes); // make the payload $payload = [ // language "language" => $this->getLanguage()->value, "image_prompt_language" => $data["image_prompt_language"], // title "title" => $this->getTitle(), // global systems "has_global_systems_text" => !empty(Settings::get("global_systems.text_instructions")), "global_systems_text" => Settings::get("global_systems.text_instructions"), // generation brief "has_generation_brief" => !empty($data["generation_brief"]), "generation_brief" => $data["generation_brief"], // custom instructions "has_text_custom_instructions" => !empty($this->getModuleValue("content.custom_instructions.text")), "text_custom_instructions" => $this->getModuleValue("content.custom_instructions.text"), // content options "has_lists_enabled" => $this->getModuleValue("content.automatic.lists.enabled", false), "has_tables_enabled" => $this->getModuleValue("content.automatic.tables.enabled", false), "has_bold_words_enabled" => $this->getModuleValue("content.automatic.bold_words.enabled", false), "has_emojis_enabled" => $this->getModuleValue("content.automatic.emojis.enabled", false), "has_summary_enabled" => $this->getModuleValue("content.automatic.summary.enabled", false), "has_brief_enabled" => $this->getModuleValue("content.automatic.brief.enabled", false), "has_faq_enabled" => $this->getModuleValue("content.automatic.faq.enabled", false), "has_toolbox_automatic_enabled" => $this->getModuleValue("content.automatic.toolbox.enabled", false) && $this->getModuleValue("content.automatic.toolbox.template", "automatic") === "automatic", "has_toolbox_charts_enabled" => $this->getModuleValue("content.automatic.toolbox.enabled", false) && $this->getModuleValue("content.automatic.toolbox.template", "automatic") === "charts", "has_toolbox_comparison_table_enabled" => $this->getModuleValue("content.automatic.toolbox.enabled", false) && $this->getModuleValue("content.automatic.toolbox.template", "automatic") === "comparison_table", "has_toolbox_timeline_enabled" => $this->getModuleValue("content.automatic.toolbox.enabled", false) && $this->getModuleValue("content.automatic.toolbox.template", "automatic") === "timeline", "has_toolbox_treemap_enabled" => $this->getModuleValue("content.automatic.toolbox.enabled", false) && $this->getModuleValue("content.automatic.toolbox.template", "automatic") === "treemap", "has_toolbox_quizz_enabled" => $this->getModuleValue("content.automatic.toolbox.enabled", false) && $this->getModuleValue("content.automatic.toolbox.template", "automatic") === "quizz", "has_toolbox_infographic_enabled" => $this->getModuleValue("content.automatic.toolbox.enabled", false) && $this->getModuleValue("content.automatic.toolbox.template", "automatic") === "infographic", "has_toolbox_simulator_enabled" => $this->getModuleValue("content.automatic.toolbox.enabled", false) && $this->getModuleValue("content.automatic.toolbox.template", "automatic") === "simulator", "has_toolbox_calculator_enabled" => $this->getModuleValue("content.automatic.toolbox.enabled", false) && $this->getModuleValue("content.automatic.toolbox.template", "automatic") === "calculator", "has_toolbox_converter_enabled" => $this->getModuleValue("content.automatic.toolbox.enabled", false) && $this->getModuleValue("content.automatic.toolbox.template", "automatic") === "converter", "has_amazon_enabled" => $this->getModuleValue("content.automatic.amazon.enabled", false), // content length "has_small_text_enabled" => $this->getModuleValue("content.automatic.text_length", "medium") === "small", "has_medium_text_enabled" => $this->getModuleValue("content.automatic.text_length", "medium") === "medium", "has_large_text_enabled" => $this->getModuleValue("content.automatic.text_length", "medium") === "large", // keywords "has_keywords" => !empty($data["keywords"]), "keywords" => implode("\n", $data["keywords"]), // persona "has_persona" => !empty($data["persona"]), "persona" => $data["persona"], "persona_name" => $data["persona_name"], "persona_age" => $data["persona_age"], "persona_job" => $data["persona_job"], "persona_style" => $data["persona_style"], "has_persona_custom_instructions" => !empty($data["persona_custom_instructions"]), "persona_custom_instructions" => $data["persona_custom_instructions"], // writing style "has_writing_style" => !empty($data["writing_style"]), "writing_style" => $data["writing_style"], // images "has_images_enabled" => $this->getModuleValue("content.automatic.images.enabled", false), "max_content_images" => $this->getModuleValue("content.automatic.images.max_images", 1), "has_images_custom_instructions" => !empty($this->getModuleValue("content.custom_instructions.images")), "images_custom_instructions" => $this->getModuleValue("content.custom_instructions.images"), // search intentions "has_search_intentions" => !empty($data["search_intentions"]), "search_intentions" => implode("\n", $data["search_intentions"]), // content data "has_content_data" => !empty($data["content_data"]), "content_data" => implode("\n", $data["content_data"]), // external data "has_external_links" => $this->getModuleValue("content.automatic.external_links.enabled", true) && !empty($data["external_links"]) && !$this->getModuleValue("content.automatic.external_links.no_follow", false), "has_external_links_no_follow" => $this->getModuleValue("content.automatic.external_links.enabled", true) && !empty($data["external_links"]) && $this->getModuleValue("content.automatic.external_links.no_follow", false), "external_links" => implode("\n", $data["external_links"]), // internal data "has_internal_links" => $this->getModuleValue("content.automatic.internal_links.enabled", true) && !empty($data["internal_links"]) && !$this->getModuleValue("content.automatic.internal_links.no_follow", false), "has_internal_links_no_follow" => $this->getModuleValue("content.automatic.internal_links.enabled", true) && !empty($data["internal_links"]) && $this->getModuleValue("content.automatic.internal_links.no_follow", false), "internal_links" => implode("\n", $data["internal_links"]), // custom links "has_custom_links" => $this->getModuleValue("content.automatic.custom_links.enabled", false) && !empty($this->getModuleValue("content.automatic.custom_links.links", [])), "custom_links" => implode("\n", array_map(function ($link) { $o = []; if (!empty($link["anchor"])) { $o[] = "Ancre: " . $link["anchor"]; } if (!empty($link["url"])) { $o[] = "URL: " . $link["url"]; } if ($link["blank"]) { $o[] = "Target _blank: " . $link["blank"] ? "Oui" : "Non"; } if ($link["nofollow"]) { $o[] = "No follow: " . $link["nofollow"] ? "Oui" : "Non"; } return implode(" | ", $o); }, $this->getModuleValue("content.automatic.custom_links.links", []))), "has_links" => !empty($data["external_links"]) || !empty($data["internal_links"]) || ($this->getModuleValue("content.automatic.custom_links.enabled", false) && !empty($this->getModuleValue("content.automatic.custom_links.links", []))), ]; // make project types variables $projectTypesVariables = []; $projectTypes = ProjectSettingsFactory::types(); foreach ($projectTypes as $projectType) { $projectTypesVariables["has_" . $projectType . "_type_enabled"] = $this->getType() === $projectType; } $payload = array_merge($payload, $projectTypesVariables); // call the preset $response = $preset->process($payload); $html = Arr::get($response, 'choices.0.message.content'); // remove some markdown if (!empty($html)) { // convert markdown $html = trim(Support::convertMarkdown($html)); // remove emojis if disabled if (!$this->getModuleValue("content.automatic.emojis.enabled", false)) { $html = Support::removeEmojis($html); } // encode emojis for wordpress $html = wp_encode_emoji($html); } libxml_use_internal_errors(true); $this->html->loadHTML('<?xml encoding="UTF-8">' . $html, LIBXML_NOWARNING); libxml_use_internal_errors(false); // populate the ignore list $ignoreList = []; // remove image blocks if disable_image_generation is true if (!$this->getModuleValue("content.automatic.images.enabled", false)) { $ignoreList[] = "image"; } // remove youtube blocks if disable_youtube_generation is true if (!$this->getModuleValue("content.automatic.youtube.enabled", false)) { $ignoreList[] = "youtube"; } // remove embeds blocks if disable_embeds_generation is true if (!$this->getModuleValue("content.automatic.embeds.enabled", false)) { $ignoreList = array_merge($ignoreList, ["facebook", "tiktok", "instagram", "twitter"]); } // remove the ignored media elements $html = $this->removeIgnoredMediaElements($ignoreList); if (empty(trim(preg_replace("/^<body>|<\/body>$/", "", $this->html->saveHTML($this->html->getElementsByTagName("body")->item(0)))))) { throw new Exception("Text module completed with errors. No content generated."); } } /** * Generate the content with the blocks * * @return void */ private function generateWithBlocks(): void { $blocks = $this->getModuleValue("content.blocks", []); // html parts $htmlParts = []; if (count($blocks) > 0) { // make data for the preset $data = $this->makePayloadData(); $payload = [ "language" => $this->getLanguage()->value, "title" => $this->getTitle(), "request" => $this->getTitle(), "has_persona" => !empty($data["persona"]), "persona" => $data["persona"], "has_writing_style" => !empty($data["writing_style"]), "writing_style" => $data["writing_style"], ]; // count per block external and internal links $externalLinksEnabledBlocks = array_values(array_filter($blocks, function ($block) { return $block["block"] === "text" && Arr::get($block, "settings.external_links_enabled", false); })); $internalLinksEnabledBlocks = array_values(array_filter($blocks, function ($block) { return $block["block"] === "text" && Arr::get($block, "settings.internal_links_enabled", false); })); $perBlockExternalLinks = min(ceil(count($data["external_links"]) / max(1, count($externalLinksEnabledBlocks))), 5); $perBlockInternalLinks = min(ceil(count($data["internal_links"]) / max(1, count($internalLinksEnabledBlocks))), 5); $blocksPresets = []; // get the system $textBlockSystem = $this->getTextBlockSystem(); // for each blocks set by the user foreach ($blocks as $blockIndex => $block) { if ($block["block"] === "text") { $prompt = Arr::get($block, "settings.prompt", ""); if (empty($prompt)) { continue; } // create the block preset $preset = Preset::make([ "model" => $this->getModuleValue("models.text", 'gpt-4o-mini'), "messages" => [ [ "role" => "system", "content" => $textBlockSystem, ], [ "role" => "user", "content" => $prompt, ] ], ]); // create the data variable with scraped content, scraped external data and internal data $contentData = []; if (Arr::get($block, "settings.use_live_data", false)) { $contentData = $data["content_data"]; } else if (!empty($data["scraped_content_data"])) { $contentData[] = $data["scraped_content_data"]; } $contentData = implode("\n", $contentData); // create the links variable with scraped external data and internal data $links = []; if (Arr::get($block, "settings.external_links_enabled", false)) { $links = array_merge($links, array_splice($data["external_links"], 0, $perBlockExternalLinks)); } if (Arr::get($block, "settings.internal_links_enabled", false)) { $links = array_merge($links, array_splice($data["internal_links"], 0, $perBlockInternalLinks)); } shuffle($links); $links = implode("\n", $links); // add to the array of presets $blocksPresets[] = [ $preset, array_merge($payload, [ "has_data" => !empty($contentData), "data" => $contentData, "has_links" => !empty($links), "links" => $links, ]), function (array $response) use (&$htmlParts, $blockIndex, $blocks) { // callback of the preset // get the response content $content = Arr::get($response, 'choices.0.message.content'); if ($content) { $blockDocument = new DOMDocument('1.0', 'UTF-8'); $blockDocument->loadHTML('<?xml encoding="UTF-8">' . $content); $content = trim(preg_replace("/^<body>|<\/body>$/", "", $blockDocument->saveHTML($blockDocument->getElementsByTagName("body")->item(0)))); $content = trim(Support::convertMarkdown($content)); $content = wp_encode_emoji($content); $content = trim(Support::removeFootprintHeaders($content, [Str::lower($this->getTitle())])); if (!empty($content)) { $htmlParts[$blockIndex] = $content; } } } ]; } else { switch ($block["block"]) { case "image": $htmlParts[$blockIndex] = "<otoimage></otoimage>"; break; case "youtube": $htmlParts[$blockIndex] = "<otoyoutube></otoyoutube>"; break; case "facebook": $htmlParts[$blockIndex] = "<otofacebook></otofacebook>"; break; case "instagram": $htmlParts[$blockIndex] = "<otoinstagram></otoinstagram>"; break; case "twitter": $htmlParts[$blockIndex] = "<ototwitter></ototwitter>"; break; case "tiktok": $htmlParts[$blockIndex] = "<ototiktok></ototiktok>"; break; } } } // run the block preset collection if (!empty($blocksPresets)) { PresetCollection::run($blocksPresets); } } ksort($htmlParts); libxml_use_internal_errors(true); $this->html->loadHTML('<?xml encoding="UTF-8">' . implode("\n", $htmlParts), LIBXML_NOWARNING); libxml_use_internal_errors(false); } /** * Add the missing custom links * * @return void */ private function addMissingCustomLinks(): void { // verify if the custom links are enabled if (!$this->getModuleValue("content.automatic.custom_links.enabled", false)) { return; } // get the custom links $links = $this->getModuleValue("content.automatic.custom_links.links", []); // filter the links that are not in the html $links = array_values(array_filter($links, function ($link) { $xpath = new DOMXPath($this->html); $nodes = $xpath->query("//a[@href='" . $link["url"] . "']"); if ($nodes->length > 0) { return false; } return true; })); // if there is no links to add, return if (empty($links)) { return; } try { // get the preset $preset = Preset::findFromAPI("generate_custom_external_link_section"); // make the payloads $payloads = []; foreach ($links as $link) { // get url title $client = new Client( [ "headers" => [ "User-Agent" => 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/118.0.0.0 Safari/537.36', ] ] ); $response = $client->get($link["url"]); $response = $response->getBody()->getContents(); $dom = new DOMDocument(); @$dom->loadHTML($response); $xpath = new DOMXPath($dom); $title = null; $nodes = $xpath->query('//h1'); if ($nodes->count() > 0) { $title = $nodes[0]->textContent; } else { $nodes = $xpath->query('//title'); if ($nodes->count() > 0) { $title = $nodes[0]->textContent; } } $payloads[] = [ "language" => $this->getLanguage()->value, "source_title" => empty($title) ? $this->getTitle() : $title, "source_url" => $link["url"], "source_anchor" => $link["anchor"], "has_blank_option" => $link["blank"], "has_nofollow_option" => $link["nofollow"], ]; } // process the pool $responses = $preset->processPool($payloads); // get the responses html content $responses = array_map(function ($response) { $content = Arr::get($response, 'choices.0.message.content'); if (!empty($content)) { $content = trim(Support::convertMarkdown($content)); } return $content; }, $responses); // get the nodes $xpath = new DOMXPath($this->html); $nodes = $xpath->query("//h2"); if ($nodes->length === 0) { $nodes = $xpath->query("//p"); } // if there is responses and nodes, add the links if (!empty($responses) && $nodes->length > 0) { // create the indexes by taking alternately the first and last node $indexes = []; $prepend = false; foreach ($nodes as $index => $node) { if ($prepend) { array_unshift($indexes, $index); } else { array_push($indexes, $index); } $prepend = !$prepend; } // chunk the responses $chunks = (new Collection($responses))->chunk(ceil(count($responses) / $nodes->length))->toArray(); // for each chunk, add the links foreach ($chunks as $index => $chunk) { $position = $indexes[$index]; // create the DomElements $elements = []; foreach ($chunk as $response) { $fragment = $this->html->createDocumentFragment(); $fragment->appendXML($response); $elements[] = $fragment; } // if the position is the last node, add the elements to the last node if ($position === $nodes->length - 1) { foreach ($elements as $element) { $nodes->item($nodes->length - 1)->appendChild($element); } } else { // add the elements to the node after the position foreach ($elements as $element) { $nodes->item($position + 1)->parentNode->insertBefore($element, $nodes->item($position + 1)); } } } } } catch (Exception $e) { } } /** * Make the payload data * * @return array */ private function makePayloadData(): array { $data = [ "persona" => null, "persona_name" => null, "persona_age" => null, "persona_job" => null, "persona_style" => null, "persona_custom_instructions" => null, "writing_style" => null, "keywords" => [], "content_data" => [], "scraped_content_data" => null, "external_links" => [], "search_intentions" => [], "internal_links" => [], "generation_brief" => null, "image_prompt_language" => "anglais", ]; // get data from url or youtube video switch ($this->getType()) { case "news": case "news-now": case "rss": case "rss-now": case "sitemap": case "url": $content = ContentData::getUrlContent(Arr::get($this->publication->meta, "url")); if (!empty($content)) { $data["content_data"][] = $content; $data["scraped_content_data"] = $content; } break; case "youtube": $captions = ContentData::getYoutubeVideoCaptions(Arr::get($this->publication->meta, "videoId"), $this->getLanguage()); if (!empty($captions)) { $data["content_data"][] = $captions; $data["scraped_content_data"] = $captions; } break; } // persona $persona = $this->getPersona(); if ($persona) { $data["persona"] = $persona->instructions; $data["persona_name"] = $persona->user_full_name; $data["persona_age"] = $persona->age; $data["persona_job"] = $persona->job; $data["persona_style"] = $persona->writing_style; $data["persona_custom_instructions"] = $persona->custom_instructions; } if (!empty($this->getModuleValue("content.custom_instructions.profile", ""))) { $data["persona_custom_instructions"] = $this->getModuleValue("content.custom_instructions.profile", ""); } // writing style $data["writing_style"] = ContentData::getWritingStyle($this->getModuleValue("content.writing_style")); // get external data $externalData = ContentData::getExternalData($this->getTitle(), $this->getLanguage()); shuffle($externalData); $data["external_links"] = array_merge($data["external_links"], array_values(array_filter(Arr::pluck($externalData, "link")))); // get internal data $internalData = ContentData::getInternalData($this->getTitle(), $this->getLanguage(), $this->publication->post_id); shuffle($internalData); $data["internal_links"] = array_merge($data["internal_links"], array_values(array_filter(Arr::pluck($internalData, "link")))); // get search intentions $data["search_intentions"] = array_values(array_filter(Arr::pluck($externalData, "content"))); // get a random url content from external links if (!in_array($this->getType(), [ "news", "news-now", "rss", "rss-now", "sitemap", "url", "youtube", ])) { // first get the custom links $urls = $this->getModuleValue("content.data.links", []); $urls = array_values(array_filter($urls, function ($url) { return $url !== null && !empty(trim($url)); })); // if there is no custom links, get the competitors links if (empty($urls) && !empty($data["external_links"])) { $urls = Arr::random($data["external_links"], min(2, count($data["external_links"]))); } foreach ($urls as $url) { try { $url = trim($url); if (empty($url)) { continue; } $content = ContentData::getUrlContent($url); $content = trim($content); if (!empty($content)) { $data["content_data"][] = $content; } } catch (Exception $e) { } } } // add generation brief if (!empty(trim($this->getModuleValue("content.generation_brief", "")))) { $data["generation_brief"] = $this->getModuleValue("content.generation_brief", ""); } // keywords if ($this->getModuleValue("content.automatic.keywords.automatic.enabled", false)) { $data["keywords"] = ContentData::getKeywords($this->getTitle(), $this->getLanguage()); } else { $data["keywords"] = $this->getModuleValue("content.automatic.keywords.custom", []); } // image prompt language if (Arr::get(Image::imageModelToProviderAndModel($this->getModuleValue("models.image")), "provider") === "dall_e") { $data["image_prompt_language"] = $this->getLanguage()->value; } return $data; } /** * Remove the ignored media elements from the html * * @param array $ignore * @return void */ private function removeIgnoredMediaElements(array $ignore = []) { $elements = [ "image" => ["otoimage", "image", "img"], "youtube" => "otoyoutube", "facebook" => "otofacebook", "instagram" => "otoinstagram", "tiktok" => "ototiktok", "twitter" => "ototwitter", ]; // create xpath $xpath = new DOMXPath($this->html); foreach ($elements as $elementKey => $tagName) { // get the nodes if (is_array($tagName)) { $nodes = $xpath->query(implode(" | ", array_map(function ($tag) { return "//{$tag}"; }, $tagName))); } else { $nodes = $xpath->query("//{$tagName}"); } foreach ($nodes as $node) { // verify if the element is in the ignore list if (in_array($elementKey, $ignore)) { // remove the element $node->parentNode->removeChild($node); } } } } /** * get the text block system from cache or from the api * * @return string */ private function getTextBlockSystem(): string { $prompt = implode("\n", [ "=====================", "Instructions de génération:", "=====================", "La sortie du texte doit être en \"[LANGUAGE]\" et en HTML. C'est très important.", "---", "[IF;HAS_PERSONA]", "Ce texte est rédigé par: [PERSONA]", "Ne cite pas le persona qui a ecrit ce texte dans le paragraphe.", "---", "[ENDIF]", "[IF;HAS_BUYER_PERSONA]", "Ce texte cible ce type de buyer persona, voici un exemple de buyer persona: [BUYER_PERSONA]", "Important : ce n'est qu'un exemple ne buyer persona. Ne reprend et ne cite pas le persona ou le nom cité dans l'exemple.", "---", "[ENDIF]", "[IF;HAS_WRITING_STYLE]", "Ecrit avec ce style d'écriture : [WRITING_STYLE]", "---", "[ENDIF]", "[IF;HAS_DATA]", "Utilise des élément de ce texte pour alimenter l'article. Il est important que l'article soit différent du texte source, en changeant au maximum e style du texte: ", "[DATA]", "---", "[ENDIF]", "[IF;HAS_LINKS]", "Inclu ces lien dans le texte de, maniere naturel.", "[LINKS]", "---", "[ELSE]", "N'ajoute pas de lien externe (https// ...) dans le texte ! ", "---", "[ENDIF]", "Enlève le formatage Markdown du texte, ne décrit pas ce que tu génères.", "---", "Highlight important keywords in <strong>.", "=====================", ]); $remoteTextBlockSystem = DataSync::getData("text_block_system"); if (!empty($remoteTextBlockSystem) && !empty(Arr::get($remoteTextBlockSystem, "prompt"))) { $prompt = Arr::get($remoteTextBlockSystem, "prompt"); } return $prompt; } /** * Determine if the module is runnable * * @return boolean */ public function isRunnable(): bool { return true; } static public function isEnabled(Publishable $publishable): bool { return true; } }