diff --git a/ai_auto_reference.action.yml b/ai_auto_reference.action.yml new file mode 100644 index 0000000000000000000000000000000000000000..9343e794bb6cdfa2fcfaeededa256de08d2a56cd --- /dev/null +++ b/ai_auto_reference.action.yml @@ -0,0 +1,5 @@ +entity.ai_auto_reference_prompt.add_form: + route_name: 'entity.ai_auto_reference_prompt.add_form' + title: 'Add AI Auto-reference Prompt' + appears_on: + - entity.ai_auto_reference_prompt.collection diff --git a/ai_auto_reference.install b/ai_auto_reference.install index bf5641ed83c92774fdc7d5530f23638e66e817c7..935a03120d26ab6b1b1de6f0a12c2beba2932a51 100644 --- a/ai_auto_reference.install +++ b/ai_auto_reference.install @@ -2,11 +2,28 @@ /** * @file - * Update scripts for the AI Auto Reference module. + * Update scripts for the AI Auto-reference module. */ +use Drupal\ai_auto_reference\Entity\AiAutoReferencePrompt; use Drupal\node\Entity\NodeType; +/** + * Implements hook_install(). + */ +function ai_auto_reference_install($is_syncing) { + AiAutoReferencePrompt::create([ + 'id' => 'default_multiple_prompt', + 'label' => t('Default prompt for multiple target references'), + 'prompt' => 'For the contents within the code block: \r\n```{CONTENTS}```\r\n\r\nWhich two to four of the following `|` separated options are highly relevant and moderately relevant?\r\n```{POSSIBLE_RESULTS}```\r\n\r\nReturn selections from within the square brackets only and as a valid RFC8259 compliant JSON array within two array keys `highly` and `moderately` without deviation.\r\nExample:\r\n{\r\n \"highly\": [\r\n \"First suggestion\",\r\n \"Second suggestion\"\r\n ],\r\n \"moderately\": [\r\n \"Third suggestion\",\r\n \"Fourth suggestion\"\r\n ]\r\n}', + ])->save(); + AiAutoReferencePrompt::create([ + 'id' => 'default_single_prompt', + 'label' => t('Default prompt for single target reference'), + 'prompt' => 'For the contents within the code block: \r\n```{CONTENTS}```\r\n\r\nWhich one of the following `|` separated options are highly relevant and moderately relevant?\r\n```{POSSIBLE_RESULTS}```\r\n\r\nReturn a single selection from within the square brackets only and as a valid RFC8259 compliant JSON array within one array key `highly` without deviation.\r\nExample:\r\n{\r\n \"highly\": [\r\n \"Your suggestion here\"\r\n ],\r\n}', + ])->save(); +} + /** * Add relevance levels to the default configuration settings. */ @@ -58,3 +75,54 @@ function ai_auto_reference_update_10002() { } } } + +/** + * Create default prompts to support prompt management. + */ +function ai_auto_reference_update_10003() { + // Create the AI Auto-reference Prompt entity. + $changeList = \Drupal::entityDefinitionUpdateManager()->getChangeList(); + if (array_key_exists('ai_auto_reference_prompt', $changeList)) { + $entity_type = \Drupal::entityTypeManager()->getDefinition('ai_auto_reference_prompt'); + \Drupal::entityDefinitionUpdateManager()->installEntityType($entity_type); + } + + // Create default prompts. + $prompts = AiAutoReferencePrompt::loadMultiple(); + if (empty($prompts)) { + // Create a default prompts. + $prompts['default_multiple_prompt'] = AiAutoReferencePrompt::create([ + 'id' => 'default_multiple_prompt', + 'label' => t('Default prompt for multiple target references'), + 'prompt' => 'For the contents within the code block: \r\n```{CONTENTS}```\r\n\r\nWhich two to four of the following `|` separated options are highly relevant and moderately relevant?\r\n```{POSSIBLE_RESULTS}```\r\n\r\nReturn selections from within the square brackets only and as a valid RFC8259 compliant JSON array within two array keys `highly` and `moderately` without deviation.\r\nExample:\r\n{\r\n \"highly\": [\r\n \"First suggestion\",\r\n \"Second suggestion\"\r\n ],\r\n \"moderately\": [\r\n \"Third suggestion\",\r\n \"Fourth suggestion\"\r\n ]\r\n}', + ])->save(); + $prompts['default_single_prompt'] = AiAutoReferencePrompt::create([ + 'id' => 'default_single_prompt', + 'label' => t('Default prompt for single target reference'), + 'prompt' => 'For the contents within the code block: \r\n```{CONTENTS}```\r\n\r\nWhich one of the following `|` separated options are highly relevant and moderately relevant?\r\n```{POSSIBLE_RESULTS}```\r\n\r\nReturn a single selection from within the square brackets only and as a valid RFC8259 compliant JSON array within one array key `highly` without deviation.\r\nExample:\r\n{\r\n \"highly\": [\r\n \"Your suggestion here\"\r\n ],\r\n}', + ])->save(); + } + + // Update existing node bundle auto-reference configurations to use a default + // prompt. + $bundles = NodeType::loadMultiple(); + if (!$bundles) { + return; + } + $prompt_keys = array_keys($prompts); + $prompt_key = reset($prompt_keys); + foreach ($bundles as $bundle) { + /** @var \Drupal\Core\Entity\Display\EntityDisplayInterface $form_display_entity */ + $form_display_entity = \Drupal::entityTypeManager() + ->getStorage('entity_form_display') + ->load('node.' . $bundle->id() . '.default'); + $settings = $form_display_entity->getThirdPartySettings('ai_auto_reference'); + if (!empty($settings)) { + foreach ($settings as $field_name => $setting) { + $setting['prompt'] = $prompt_key; + $form_display_entity->setThirdPartySetting('ai_auto_reference', $field_name, $setting); + $form_display_entity->save(); + } + } + } +} diff --git a/ai_auto_reference.links.menu.yml b/ai_auto_reference.links.menu.yml index 1eebe2b190a51addedc54c0582ab52ee11197574..334da82e0dfc0d366a16cf2c511281bf26ad6f8e 100644 --- a/ai_auto_reference.links.menu.yml +++ b/ai_auto_reference.links.menu.yml @@ -4,3 +4,10 @@ ai_auto_reference.ai_auto_reference_settings_form: description: 'Administer settings related to the AI Auto-reference module.' parent: system.admin_config_content weight: 99 + +entity.ai_auto_reference_prompt.collection: + title: 'AI Auto-reference Prompts' + description: 'Manage AI-generated reference prompts' + route_name: entity.ai_auto_reference_prompt.collection + parent: system.admin_config_content + weight: 100 diff --git a/ai_auto_reference.links.task.yml b/ai_auto_reference.links.task.yml index 51a742270111e1a5bfab3f863585962c434a89e8..7b5011d69822c5698bbff3c57ac8a24216af7440 100644 --- a/ai_auto_reference.links.task.yml +++ b/ai_auto_reference.links.task.yml @@ -2,8 +2,14 @@ ai_auto_reference.ai_auto_reference_settings_form_tab: route_name: ai_auto_reference.ai_auto_reference_settings_form title: 'AI Auto-reference settings' base_route: ai_auto_reference.ai_auto_reference_settings_form + weight: 14 ai_auto_reference.node_bundle: title: AI autoreference route_name: ai_auto_reference.node_bundle.settings base_route: entity.node_type.edit_form weight: 10 +entity.ai_auto_reference_prompt.collection: + title: 'AI Auto-reference Prompts' + route_name: entity.ai_auto_reference_prompt.collection + base_route: ai_auto_reference.ai_auto_reference_settings_form + weight: 15 \ No newline at end of file diff --git a/ai_auto_reference.routing.yml b/ai_auto_reference.routing.yml index 58373152b021cd1b110d3c9f7ad01c6666c7bee7..ef223aba4b3ad1727acd0eb9fcc8d869a2a8dafe 100644 --- a/ai_auto_reference.routing.yml +++ b/ai_auto_reference.routing.yml @@ -1,3 +1,4 @@ +# Settings form. ai_auto_reference.ai_auto_reference_settings_form: path: '/admin/config/content/ai-autoreference-settings' defaults: @@ -7,6 +8,8 @@ ai_auto_reference.ai_auto_reference_settings_form: _permission: 'administer ai autoreference' options: _admin_route: TRUE + +# Per node bundle configuration. ai_auto_reference.node_bundle.settings: path: '/admin/structure/types/manage/{node_type}/ai-autoreference' defaults: @@ -14,6 +17,9 @@ ai_auto_reference.node_bundle.settings: _title: 'AI Auto-reference' requirements: _permission: 'administer ai autoreference' + options: + _admin_route: TRUE + ai_auto_reference.edit_form: path: '/admin/structure/types/manage/{node_type}/ai-autoreference/{field_name}/edit' defaults: @@ -21,6 +27,8 @@ ai_auto_reference.edit_form: _form: '\Drupal\ai_auto_reference\Form\AutoReferenceEditForm' requirements: _permission: 'administer ai autoreference' + options: + _admin_route: TRUE ai_auto_reference.delete_form: path: '/admin/structure/types/manage/{node_type}/ai-autoreference/{field_name}/delete' @@ -28,3 +36,46 @@ ai_auto_reference.delete_form: _form: '\Drupal\ai_auto_reference\Form\AutoReferenceDeleteForm' requirements: _permission: 'administer ai autoreference' + options: + _admin_route: TRUE + +# Prompt management. +entity.ai_auto_reference_prompt.collection: + path: '/admin/config/content/ai-autoreference-settings/prompts' + defaults: + _entity_list: 'ai_auto_reference_prompt' + _title: 'AI Auto-reference Prompts' + requirements: + _permission: 'administer ai autoreference' + options: + _admin_route: TRUE + +entity.ai_auto_reference_prompt.add_form: + path: '/admin/config/content/ai-autoreference-settings/prompts/add' + defaults: + _entity_form: 'ai_auto_reference_prompt.add' + _title: 'Add AI Auto-reference Prompt' + requirements: + _permission: 'administer ai autoreference' + options: + _admin_route: TRUE + +entity.ai_auto_reference_prompt.edit_form: + path: '/admin/config/content/ai-autoreference-settings/prompts/{ai_auto_reference_prompt}' + defaults: + _entity_form: 'ai_auto_reference_prompt.edit' + _title: 'Edit AI Auto-reference Prompt' + requirements: + _permission: 'administer ai autoreference' + options: + _admin_route: TRUE + +entity.ai_auto_reference_prompt.delete_form: + path: '/admin/config/content/ai-autoreference-settings/prompts/{ai_auto_reference_prompt}/delete' + defaults: + _entity_form: 'ai_auto_reference_prompt.delete' + _title: 'Delete AI Auto-reference Prompt' + requirements: + _permission: 'administer ai autoreference' + options: + _admin_route: TRUE diff --git a/config/install/ai_auto_reference.settings.yml b/config/install/ai_auto_reference.settings.yml index 44b6c5b198f18c952559c86ff682f308ff089982..1d19f831b1f53dae4226d46917b1f0ebeff2ac83 100644 --- a/config/install/ai_auto_reference.settings.yml +++ b/config/install/ai_auto_reference.settings.yml @@ -1,6 +1,6 @@ service: "gpt" service_model: "gpt-3.5-turbo" service_api_key: "" -token_limit: 4000 +token_limit: 8000 auto_apply_suggestions: false auto_apply_relevance_levels: ["high","medium"] diff --git a/config/schema/ai_auto_reference.entity_display.schema.yml b/config/schema/ai_auto_reference.entity_display.schema.yml index d63d68243b0f35ddc41d9f4e91b663c88b551c71..0c163ff70958a069899d85dd96600bde99ebf46f 100644 --- a/config/schema/ai_auto_reference.entity_display.schema.yml +++ b/config/schema/ai_auto_reference.entity_display.schema.yml @@ -8,6 +8,9 @@ core.entity_view_display.*.*.*.third_party.ai_auto_reference: view_mode: type: string label: 'The view mode' + prompt: + type: string + label: 'The prompt to use' core.entity_form_display.*.*.*.third_party.ai_auto_reference: type: sequence @@ -19,3 +22,6 @@ core.entity_form_display.*.*.*.third_party.ai_auto_reference: view_mode: type: string label: 'The view mode' + prompt: + type: string + label: 'The prompt to use' diff --git a/config/schema/ai_auto_reference.prompt.schema.yml b/config/schema/ai_auto_reference.prompt.schema.yml new file mode 100644 index 0000000000000000000000000000000000000000..3511dc3bbb2f8ebb3184c22327524356f21a03f1 --- /dev/null +++ b/config/schema/ai_auto_reference.prompt.schema.yml @@ -0,0 +1,13 @@ +ai_auto_reference.ai_auto_reference_prompt.*: + type: config_entity + label: 'AI Auto-reference Prompt' + mapping: + id: + type: string + label: 'ID' + label: + type: label + label: 'Label' + prompt: + type: text + label: 'Prompt' diff --git a/src/AiAutoReferencePromptListBuilder.php b/src/AiAutoReferencePromptListBuilder.php new file mode 100644 index 0000000000000000000000000000000000000000..51b73b1b84b1719654cb183ec66ed99dfd740448 --- /dev/null +++ b/src/AiAutoReferencePromptListBuilder.php @@ -0,0 +1,53 @@ +<?php + +namespace Drupal\ai_auto_reference; + +use Drupal\Core\Config\Entity\ConfigEntityListBuilder; +use Drupal\Core\Entity\EntityInterface; +use Drupal\Core\Url; + +/** + * Provides a listing of AI Auto-reference Prompts. + */ +class AiAutoReferencePromptListBuilder extends ConfigEntityListBuilder { + + /** + * {@inheritdoc} + */ + public function buildHeader() { + $header['label'] = $this->t('AI Auto-reference Prompt'); + $header['id'] = $this->t('Machine name'); + return $header + parent::buildHeader(); + } + + /** + * {@inheritdoc} + */ + public function buildRow(EntityInterface $entity) { + $row['label'] = $entity->label(); + $row['id'] = $entity->id(); + return $row + parent::buildRow($entity); + } + + /** + * {@inheritdoc} + */ + public function render() { + $build = parent::render(); + + // Add "Add AI Auto-reference Prompt" button. + $build['actions'] = [ + '#type' => 'container', + '#attributes' => ['class' => ['entity-add-link']], + 'add' => [ + '#type' => 'link', + '#title' => $this->t('Add Prompt'), + '#url' => Url::fromRoute('entity.ai_auto_reference_prompt.add_form'), + '#attributes' => ['class' => ['button', 'button--primary']], + ], + ]; + + return $build; + } + +} diff --git a/src/AiReferenceGenerator.php b/src/AiReferenceGenerator.php index 46aa30559eca83781b7943c8479f64528ef05efc..0983393609162bd8eb02bdced62c31243e62cff7 100644 --- a/src/AiReferenceGenerator.php +++ b/src/AiReferenceGenerator.php @@ -7,6 +7,7 @@ use Drupal\ai\OperationType\Chat\ChatInput; use Drupal\ai\OperationType\Chat\ChatMessage; use Drupal\ai\Utility\TextChunkerInterface; use Drupal\ai\Utility\TokenizerInterface; +use Drupal\ai_auto_reference\Entity\AiAutoReferencePromptInterface; use Drupal\Component\Serialization\Json; use Drupal\Core\Config\ConfigFactoryInterface; use Drupal\Core\Config\ImmutableConfig; @@ -94,10 +95,11 @@ class AiReferenceGenerator { $ai_references = []; foreach ($settings as $field_name => $setting) { // Add AI autogenerate button only if at least one field is configured. - if (!empty($setting['view_mode'])) { + if (!empty($setting['view_mode']) && !empty($setting['prompt'])) { $ai_references[] = [ 'field_name' => $field_name, 'view_mode' => $setting['view_mode'], + 'prompt' => $setting['prompt'], ]; } } @@ -114,11 +116,18 @@ class AiReferenceGenerator { * Field machine name. * @param string $view_mode * Entity view mode. + * @param string $prompt + * The prompt machine name. * * @return array * Array with suggestions. */ - public function getAiSuggestions(NodeInterface $node, $field_name, $view_mode) { + public function getAiSuggestions( + NodeInterface $node, + string $field_name, + string $view_mode, + string $prompt, + ) { $config = $this->config->get('ai_auto_reference.settings'); $chat_model_id = $this->aiProviderManager->getModelNameFromSimpleOption($config->get('provider')); $chat_model_id = $chat_model_id ?: 'gpt-3.5'; @@ -157,13 +166,21 @@ class AiReferenceGenerator { $this->themeManager->setActiveTheme($active_theme); // Build the prompt to send. - // @todo allow configuration of the prompt. - $prompt = "For the contents within the block:\n```{CONTENTS}\n```\n\n"; - $prompt .= "Which two to four of the following `|` separated options are highly relevant and moderately relevant?\n```{POSSIBLE_RESULTS}\n```\n\n"; - $prompt .= "Return selections from within the square brackets only and as a valid RFC8259 compliant JSON array within two array keys `highly` and `moderately` without deviation.\n\n"; + $prompt = $this->entityTypeManager->getStorage('ai_auto_reference_prompt')->load($prompt); + if ($prompt instanceof AiAutoReferencePromptInterface) { + // Retrieve prompt from configuration. + $prompt_text = $prompt->getPrompt(); + } + else { + // Fallback in case the target prompt was deleted. + $prompt_text = "For the contents within the block:\n```{CONTENTS}\n```\n\n"; + $prompt_text .= "Which two to four of the following `|` separated options are highly relevant and moderately relevant?\n```{POSSIBLE_RESULTS}\n```\n\n"; + $prompt_text .= "Return selections from within the square brackets only and as a valid RFC8259 compliant JSON array within two array keys `highly` and `moderately` without deviation.\n\n"; + $prompt_text .= "Example:\r\n{\r\n \"highly\": [\r\n \"First suggestion\",\r\n \"Second suggestion\"\r\n ],\r\n \"moderately\": [\r\n \"Third suggestion\",\r\n \"Fourth suggestion\"\r\n ]\r\n}"; + } // If the prompt is longer than the limit, get a summary of the contents. - $prompt_length = $this->tokenizer->countTokens($prompt); + $prompt_length = $this->tokenizer->countTokens($prompt_text); $possible_results_length = $this->tokenizer->countTokens($imploded_possible_results); $contents_length = $this->tokenizer->countTokens($contents); if ($prompt_length + $possible_results_length > $token_limit) { @@ -199,11 +216,11 @@ class AiReferenceGenerator { } // Run replacements in the prompt. - $prompt = str_replace('{POSSIBLE_RESULTS}', $imploded_possible_results, $prompt); - $prompt = str_replace('{CONTENTS}', $contents, $prompt); + $prompt_text = str_replace('{POSSIBLE_RESULTS}', $imploded_possible_results, $prompt_text); + $prompt_text = str_replace('{CONTENTS}', $contents, $prompt_text); // Get the array of 'highly' and 'moderately' related results back. - if ($answer = $this->aiApiCall($config, $prompt)) { + if ($answer = $this->aiApiCall($config, $prompt_text)) { $answer = Json::decode($answer); $suggestions['h'] = !empty($answer['highly']) ? array_keys(array_intersect($possible_results, $answer['highly'])) : []; $suggestions['m'] = !empty($answer['moderately']) ? array_keys(array_intersect($possible_results, $answer['moderately'])) : []; @@ -255,18 +272,18 @@ class AiReferenceGenerator { * * @param \Drupal\Core\Config\ImmutableConfig $config * AI auto-reference config. - * @param string $prompt + * @param string $prompt_text * AI prompt. * * @return string * AI answer. */ - public function aiApiCall(ImmutableConfig $config, string $prompt): string { + public function aiApiCall(ImmutableConfig $config, string $prompt_text): string { $provider = $config->get('provider'); $ai_provider = $this->aiProviderManager->loadProviderFromSimpleOption($provider); $ai_model = $this->aiProviderManager->getModelNameFromSimpleOption($provider); $messages = new ChatInput([ - new ChatMessage('user', $prompt), + new ChatMessage('user', $prompt_text), ]); $message = $ai_provider->chat($messages, $ai_model)->getNormalized()->getText(); $message = str_replace(['```json', '```'], '', $message); diff --git a/src/Batch/AiReferenceBatch.php b/src/Batch/AiReferenceBatch.php index e9d75db71d2d45fabe0593057db792dd35d483cc..ce00b6ef334380338d9c85710415d6111553b725 100644 --- a/src/Batch/AiReferenceBatch.php +++ b/src/Batch/AiReferenceBatch.php @@ -94,6 +94,7 @@ class AiReferenceBatch { $node, $ai_autoreference['field_name'], $ai_autoreference['view_mode'], + $ai_autoreference['prompt'], ); $imploded_suggestions = []; foreach ($suggestions as $relevance => $suggestion_ids) { diff --git a/src/Entity/AiAutoReferencePrompt.php b/src/Entity/AiAutoReferencePrompt.php new file mode 100644 index 0000000000000000000000000000000000000000..432ff7eb9b067a616d0fe49463843b75d37c979b --- /dev/null +++ b/src/Entity/AiAutoReferencePrompt.php @@ -0,0 +1,75 @@ +<?php + +namespace Drupal\ai_auto_reference\Entity; + +use Drupal\Core\Config\Entity\ConfigEntityBase; + +/** + * Defines the AI Auto-reference Prompt entity. + * + * @ConfigEntityType( + * id = "ai_auto_reference_prompt", + * label = @Translation("AI Auto-reference Prompt"), + * handlers = { + * "list_builder" = "Drupal\ai_auto_reference\AiAutoReferencePromptListBuilder", + * "form" = { + * "add" = "Drupal\ai_auto_reference\Form\AiAutoReferencePromptForm", + * "edit" = "Drupal\ai_auto_reference\Form\AiAutoReferencePromptForm", + * "delete" = "Drupal\ai_auto_reference\Form\AiAutoReferencePromptDeleteForm" + * } + * }, + * config_prefix = "ai_auto_reference_prompt", + * admin_permission = "administer ai autoreference", + * entity_keys = { + * "id" = "id", + * "label" = "label" + * }, + * config_export = { + * "id", + * "label", + * "prompt" + * }, + * links = { + * "edit-form" = "/admin/content/ai-autoreference-settings/{ai_auto_reference_prompt}", + * "delete-form" = "/admin/content/ai-autoreference-settings/{ai_auto_reference_prompt}/delete" + * } + * ) + */ +class AiAutoReferencePrompt extends ConfigEntityBase implements AiAutoReferencePromptInterface { + + /** + * The AI Auto-reference Prompt ID. + * + * @var string + */ + protected $id; + + /** + * The AI Auto-reference Prompt label. + * + * @var string + */ + protected $label; + + /** + * The AI Auto-reference Prompt text. + * + * @var string + */ + protected $prompt; + + /** + * {@inheritdoc} + */ + public function getPrompt(): string { + return $this->prompt ?? ''; + } + + /** + * {@inheritdoc} + */ + public function setPrompt($prompt): void { + $this->prompt = $prompt; + } + +} diff --git a/src/Entity/AiAutoReferencePromptInterface.php b/src/Entity/AiAutoReferencePromptInterface.php new file mode 100644 index 0000000000000000000000000000000000000000..c2bae85f85f46115b8bb5d5acf720f80e3e3e12d --- /dev/null +++ b/src/Entity/AiAutoReferencePromptInterface.php @@ -0,0 +1,28 @@ +<?php + +namespace Drupal\ai_auto_reference\Entity; + +use Drupal\Core\Config\Entity\ConfigEntityInterface; + +/** + * Provides an interface defining an AI Auto-reference Prompt entity. + */ +interface AiAutoReferencePromptInterface extends ConfigEntityInterface { + + /** + * Gets the prompt text. + * + * @return string + * The prompt text. + */ + public function getPrompt(): string; + + /** + * Sets the prompt text. + * + * @param string $prompt + * The prompt text. + */ + public function setPrompt($prompt): void; + +} diff --git a/src/Form/AiAutoReferencePromptDeleteForm.php b/src/Form/AiAutoReferencePromptDeleteForm.php new file mode 100644 index 0000000000000000000000000000000000000000..691e601836709ccc556a3d86c6482c49664b0d63 --- /dev/null +++ b/src/Form/AiAutoReferencePromptDeleteForm.php @@ -0,0 +1,47 @@ +<?php + +namespace Drupal\ai_auto_reference\Form; + +use Drupal\Core\Entity\EntityConfirmFormBase; +use Drupal\Core\Url; +use Drupal\Core\Form\FormStateInterface; + +/** + * Builds the form to delete an AI Auto-reference Prompt. + */ +class AiAutoReferencePromptDeleteForm extends EntityConfirmFormBase { + + /** + * {@inheritdoc} + */ + public function getQuestion() { + return $this->t('Are you sure you want to delete %name?', ['%name' => $this->entity->label()]); + } + + /** + * {@inheritdoc} + */ + public function getCancelUrl() { + return new Url('entity.ai_auto_reference_prompt.collection'); + } + + /** + * {@inheritdoc} + */ + public function getConfirmText() { + return $this->t('Delete'); + } + + /** + * {@inheritdoc} + */ + public function submitForm(array &$form, FormStateInterface $form_state) { + $this->entity->delete(); + $this->messenger()->addMessage($this->t('The AI Auto-reference Prompt %label has been deleted.', [ + '%label' => $this->entity->label(), + ])); + + $form_state->setRedirectUrl($this->getCancelUrl()); + } + +} diff --git a/src/Form/AiAutoReferencePromptForm.php b/src/Form/AiAutoReferencePromptForm.php new file mode 100644 index 0000000000000000000000000000000000000000..477279860d02d917319c51cf0fea73e0f36eb146 --- /dev/null +++ b/src/Form/AiAutoReferencePromptForm.php @@ -0,0 +1,116 @@ +<?php + +namespace Drupal\ai_auto_reference\Form; + +use Drupal\Core\Entity\EntityForm; +use Drupal\Core\Entity\EntityTypeManagerInterface; +use Drupal\Core\Form\FormStateInterface; +use Symfony\Component\DependencyInjection\ContainerInterface; + +/** + * Form handler for AI Auto-reference Prompt add and edit forms. + */ +class AiAutoReferencePromptForm extends EntityForm { + + /** + * Constructs an AiAutoReferencePromptForm object. + * + * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entityTypeManager + * The entity type manager. + */ + public function __construct(EntityTypeManagerInterface $entityTypeManager) { + $this->entityTypeManager = $entityTypeManager; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container) { + return new static( + $container->get('entity_type.manager') + ); + } + + /** + * {@inheritdoc} + */ + public function form(array $form, FormStateInterface $form_state) { + $form = parent::form($form, $form_state); + + $ai_auto_reference_prompt = $this->entity; + + $form['label'] = [ + '#type' => 'textfield', + '#title' => $this->t('Label'), + '#maxlength' => 255, + '#default_value' => $ai_auto_reference_prompt->label(), + '#description' => $this->t('Label for the AI Auto-reference Prompt.'), + '#required' => TRUE, + ]; + + $form['id'] = [ + '#type' => 'machine_name', + '#default_value' => $ai_auto_reference_prompt->id(), + '#machine_name' => [ + 'exists' => [$this, 'exists'], + ], + '#disabled' => !$ai_auto_reference_prompt->isNew(), + ]; + + $form['prompt'] = [ + '#type' => 'textarea', + '#title' => $this->t('Prompt'), + '#default_value' => $ai_auto_reference_prompt->getPrompt(), + '#description' => $this->t('Enter the prompt text. It must contain {CONTENTS} and {POSSIBLE_RESULTS}.'), + '#required' => TRUE, + ]; + + return $form; + } + + /** + * {@inheritdoc} + */ + public function validateForm(array &$form, FormStateInterface $form_state) { + parent::validateForm($form, $form_state); + + $prompt = $form_state->getValue('prompt'); + if (!str_contains($prompt, '{CONTENTS}') || !str_contains($prompt, '{POSSIBLE_RESULTS}')) { + $form_state->setErrorByName('prompt', $this->t('The prompt must contain both {CONTENTS} and {POSSIBLE_RESULTS}.')); + } + } + + /** + * {@inheritdoc} + */ + public function save(array $form, FormStateInterface $form_state) { + $ai_auto_reference_prompt = $this->entity; + $status = $ai_auto_reference_prompt->save(); + + if ($status === SAVED_NEW) { + $this->messenger()->addMessage($this->t('The %label AI Auto-reference Prompt has been created.', [ + '%label' => $ai_auto_reference_prompt->label(), + ])); + } + else { + $this->messenger()->addMessage($this->t('The %label AI Auto-reference Prompt has been updated.', [ + '%label' => $ai_auto_reference_prompt->label(), + ])); + } + + $form_state->setRedirect('entity.ai_auto_reference_prompt.collection'); + return $status; + } + + /** + * Helper function to check whether an AI Auto-reference Prompt entity exists. + */ + public function exists($id) { + $entity = $this->entityTypeManager->getStorage('ai_auto_reference_prompt')->getQuery() + ->condition('id', $id) + ->accessCheck() + ->execute(); + return (bool) $entity; + } + +} diff --git a/src/Form/AutoReferenceEditForm.php b/src/Form/AutoReferenceEditForm.php index 34807f45955c8c9358659f36dab45ad274beb9e8..85f8f0886c9775fb85d3a69317dc56547dd13871 100644 --- a/src/Form/AutoReferenceEditForm.php +++ b/src/Form/AutoReferenceEditForm.php @@ -115,6 +115,22 @@ class AutoReferenceEditForm extends FormBase { '#default_value' => $setting['view_mode'], '#required' => TRUE, ]; + + // Fetch available AI Auto-reference Prompts. + $prompt_options = []; + $prompt_entities = $this->entityTypeManager->getStorage('ai_auto_reference_prompt')->loadMultiple(); + foreach ($prompt_entities as $prompt) { + $prompt_options[$prompt->id()] = $prompt->label(); + } + $form['prompt'] = [ + '#type' => 'select', + '#title' => $this->t('Prompt'), + '#description' => $this->t('Select a predefined AI Auto-reference Prompt'), + '#options' => $prompt_options, + '#required' => TRUE, + '#default_value' => $setting['prompt'], + ]; + $form['bundle'] = [ '#type' => 'hidden', '#value' => $node_type, @@ -151,6 +167,7 @@ class AutoReferenceEditForm extends FormBase { $form_display_entity->setThirdPartySetting('ai_auto_reference', $field_name, [ 'view_mode' => $values['view_mode'], + 'prompt' => $values['prompt'], ]); $form_display_entity->save(); $form_state->setRedirect($this->redirectPath, ['node_type' => $bundle]); @@ -173,7 +190,7 @@ class AutoReferenceEditForm extends FormBase { if (!isset($instances[$field_name])) { return ''; } - return $this->t('Edit AI autoreference settings for %field field in %bundle content type.', [ + return $this->t('Edit AI Auto-reference settings for %field field in %bundle content type.', [ '%field' => $instances[$field_name]->getLabel(), '%bundle' => $bundle->label(), ]); diff --git a/src/Form/EntityBundleSettingsForm.php b/src/Form/EntityBundleSettingsForm.php index 2460d8f4dc6229733c210b1a721af1c873b6b1fe..9f3c7f89f11c4a64001fe9c292e760cd361de9eb 100644 --- a/src/Form/EntityBundleSettingsForm.php +++ b/src/Form/EntityBundleSettingsForm.php @@ -134,14 +134,21 @@ class EntityBundleSettingsForm extends FormBase { $entity_form_display = $this->entityTypeManager ->getStorage('entity_form_display') ->load($this->entityType . '.' . $this->bundleName . '.' . 'default'); + if (!$entity_form_display instanceof EntityFormDisplayInterface) { return; } $field_name = $table['add_new_autoreference']['field_name']; + $view_mode = $table['add_new_autoreference']['view_mode']; + $selected_prompt = $table['add_new_autoreference']['prompt']; + + // Store the selected prompt along with the field and view mode. $entity_form_display->setThirdPartySetting('ai_auto_reference', $field_name, [ - 'view_mode' => $table['add_new_autoreference']['view_mode'], + 'view_mode' => $view_mode, + 'prompt' => $selected_prompt, ]); + $entity_form_display->save(); } @@ -163,10 +170,12 @@ class EntityBundleSettingsForm extends FormBase { '#header' => [ $this->t('Target field'), $this->t('View mode'), + $this->t('Prompt'), ['data' => $this->t('Operations'), 'colspan' => 2], ], ]; - // Getting entityreference fields for current bundle. + + // Getting entity reference fields for current bundle. $bundle_fields = $this->entityFieldManager->getFieldDefinitions($this->entityType, $this->bundleName); $reference_fields = []; foreach ($bundle_fields as $field_name => $bundle_field) { @@ -194,19 +203,32 @@ class EntityBundleSettingsForm extends FormBase { $view_modes = $this->entityDisplayRepository->getViewModeOptionsByBundle($this->entityType, $this->bundleName); - // Displaying already added AI autoreference blocks. + // Fetch available AI Auto-reference Prompts. + $prompt_options = []; + $prompt_entities = $this->entityTypeManager->getStorage('ai_auto_reference_prompt')->loadMultiple(); + foreach ($prompt_entities as $prompt) { + $prompt_options[$prompt->id()] = $prompt->label(); + } + + // Displaying already added AI auto-reference blocks. $settings = $form_display_entity->getThirdPartySettings('ai_auto_reference'); foreach ($reference_fields as $field_name => $label) { - if (empty($settings[$field_name]['view_mode'])) { + if (empty($settings[$field_name]['view_mode']) || empty($settings[$field_name]['prompt'])) { continue; } + $parameters = [ 'node_type' => $this->bundleName, 'field_name' => $field_name, ]; + + // Get stored prompt label. + $stored_prompt_label = $prompt_options[$settings[$field_name]['prompt']] ?? $this->t('Unknown Prompt'); + $table[] = [ 'field_name' => ['#markup' => "{$label} ({$field_name})"], 'view_mode' => ['#markup' => $view_modes[$settings[$field_name]['view_mode']]], + 'prompt' => ['#markup' => $stored_prompt_label], 'actions' => [ '#type' => 'operations', '#links' => [ @@ -221,13 +243,11 @@ class EntityBundleSettingsForm extends FormBase { ], ], ]; - // Only single configuration is possible for a single field. + // Only allow one configuration per field. unset($reference_fields[$field_name]); } - $view_modes = $this->entityDisplayRepository->getViewModeOptionsByBundle($this->entityType, $this->bundleName); - - // Add new AI autoreference row. + // Add new AI Auto-reference row. $table['add_new_autoreference'] = [ 'field_name' => [ '#type' => 'select', @@ -252,6 +272,16 @@ class EntityBundleSettingsForm extends FormBase { '#required' => TRUE, '#attributes' => ['class' => ['conditional-fields-selector']], ], + 'prompt' => [ + '#type' => 'select', + '#title' => $this->t('Prompt'), + '#title_display' => 'invisible', + '#description' => $this->t('Select a predefined AI Auto-reference Prompt'), + '#options' => $prompt_options, + '#prefix' => '<div class="add-new-placeholder"> </div>', + '#required' => TRUE, + '#attributes' => ['class' => ['conditional-fields-selector']], + ], 'actions' => [ 'submit' => [ '#type' => 'submit', diff --git a/tests/src/Functional/AiAutoReferenceUiTest.php b/tests/src/Functional/AiAutoReferenceUiTest.php index bdb1c9a46cf9dbab37cf211969015f0b5b81d2bd..de42512a907573207c8111623db04438dc5e0540 100644 --- a/tests/src/Functional/AiAutoReferenceUiTest.php +++ b/tests/src/Functional/AiAutoReferenceUiTest.php @@ -202,11 +202,32 @@ class AiAutoReferenceUiTest extends BrowserTestBase { $this->drupalGet('node/1/edit'); $this->assertSession()->buttonNotExists('Generate references with AI'); + // Create a prompt. + $this->drupalGet('admin/config/content/ai-autoreference-settings/prompts'); + $this->clickLink('Add Prompt'); + $this->submitForm([ + 'label' => 'My prompt', + 'id' => 'my_prompt', + 'prompt' => 'This is my prompt', + ], 'Save'); + $this->assertSession()->pageTextContains('The prompt must contain both {CONTENTS} and {POSSIBLE_RESULTS}.'); + $this->submitForm([ + 'label' => 'Test create prompt', + 'id' => 'my_prompt', + 'prompt' => 'This is my prompt with {CONTENTS} and {POSSIBLE_RESULTS}', + ], 'Save'); + + // Check that it is now saved. + $this->assertSession()->elementTextContains('css', 'table', 'AI Auto-reference Prompt'); + $this->assertSession()->elementTextContains('css', 'table', 'Test create prompt'); + // Create a new auto-reference. $this->drupalGet('admin/structure/types/manage/article/ai-autoreference'); $this->submitForm([ 'table[add_new_autoreference][field_name]' => 'field_related_food', 'table[add_new_autoreference][view_mode]' => 'teaser', + // This validates that the default prompts were created on install. + 'table[add_new_autoreference][prompt]' => 'default_multiple_prompt', ], 'Add autoreference'); // Now edit it. @@ -223,6 +244,7 @@ class AiAutoReferenceUiTest extends BrowserTestBase { $this->submitForm([ 'table[add_new_autoreference][field_name]' => 'field_related_food', 'table[add_new_autoreference][view_mode]' => 'teaser', + 'table[add_new_autoreference][prompt]' => 'my_prompt', ], 'Add autoreference'); // Edit a node.