diff --git a/analyze_ai_sentiments.install b/analyze_ai_sentiments.install index b118613..5953b35 100644 --- a/analyze_ai_sentiments.install +++ b/analyze_ai_sentiments.install @@ -102,6 +102,58 @@ function analyze_ai_sentiments_install() { $database->schema()->createTable($table_name, $table_spec); } } + + // Install default sentiment configurations. + $config_factory = \Drupal::configFactory(); + $config = $config_factory->getEditable('analyze_ai_sentiments.settings'); + + // Only install defaults if no sentiments are configured. + if (empty($config->get('sentiments'))) { + $default_sentiments = [ + 'overall' => [ + 'id' => 'overall', + 'label' => 'Overall Sentiments', + 'min_label' => 'Negative', + 'mid_label' => 'Neutral', + 'max_label' => 'Positive', + 'weight' => 0, + ], + 'engagement' => [ + 'id' => 'engagement', + 'label' => 'Engagement Level', + 'min_label' => 'Passive', + 'mid_label' => 'Balanced', + 'max_label' => 'Interactive', + 'weight' => 1, + ], + 'trust' => [ + 'id' => 'trust', + 'label' => 'Trust/Credibility', + 'min_label' => 'Promotional', + 'mid_label' => 'Balanced', + 'max_label' => 'Authoritative', + 'weight' => 2, + ], + 'objectivity' => [ + 'id' => 'objectivity', + 'label' => 'Objectivity', + 'min_label' => 'Subjective', + 'mid_label' => 'Mixed', + 'max_label' => 'Objective', + 'weight' => 3, + ], + 'complexity' => [ + 'id' => 'complexity', + 'label' => 'Technical Complexity', + 'min_label' => 'Basic', + 'mid_label' => 'Moderate', + 'max_label' => 'Complex', + 'weight' => 4, + ], + ]; + + $config->set('sentiments', $default_sentiments)->save(); + } } /** diff --git a/analyze_ai_sentiments.links.action.yml b/analyze_ai_sentiments.links.action.yml new file mode 100644 index 0000000..d7f6dea --- /dev/null +++ b/analyze_ai_sentiments.links.action.yml @@ -0,0 +1,5 @@ +analyze_ai_sentiments.sentiment.add: + route_name: analyze_ai_sentiments.add_sentiments + title: 'Add sentiment' + appears_on: + - analyze_ai_sentiments.settings diff --git a/src/Form/AddSentimentsForm.php b/src/Form/AddSentimentsForm.php index 88d5d37..2ad6dda 100644 --- a/src/Form/AddSentimentsForm.php +++ b/src/Form/AddSentimentsForm.php @@ -6,6 +6,7 @@ use Drupal\Core\Form\FormBase; use Drupal\Core\Form\FormStateInterface; use Drupal\Core\Config\ConfigFactoryInterface; +use Drupal\analyze_ai_sentiments\Service\SentimentsStorageService; use Symfony\Component\DependencyInjection\ContainerInterface; /** @@ -19,14 +20,27 @@ class AddSentimentsForm extends FormBase { */ protected $configFactory; + /** + * The sentiments storage service. + * + * @var \Drupal\analyze_ai_sentiments\Service\SentimentsStorageService + */ + protected $sentimentsStorage; + /** * Constructs a new AddSentimentsForm. * * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory * The config factory. + * @param \Drupal\analyze_ai_sentiments\Service\SentimentsStorageService $sentiments_storage + * The sentiments storage service. */ - public function __construct(ConfigFactoryInterface $config_factory) { + public function __construct( + ConfigFactoryInterface $config_factory, + SentimentsStorageService $sentiments_storage, + ) { $this->configFactory = $config_factory; + $this->sentimentsStorage = $sentiments_storage; } /** @@ -35,7 +49,8 @@ public function __construct(ConfigFactoryInterface $config_factory) { public static function create(ContainerInterface $container): static { /** @var static */ return new self( - $container->get('config.factory') + $container->get('config.factory'), + $container->get('analyze_ai_sentiments.storage') ); } @@ -56,9 +71,7 @@ public function getFormId() { * TRUE if the sentiments exists, FALSE otherwise. */ public function sentimentsExists($id) { - $config = $this->configFactory->get('analyze_ai_sentiments.settings'); - $sentiments = $config->get('sentiments') ?: []; - return isset($sentiments[$id]); + return $this->sentimentsStorage->sentimentExists($id); } /** @@ -157,27 +170,25 @@ public function buildForm(array $form, FormStateInterface $form_state): array { */ public function submitForm(array &$form, FormStateInterface $form_state): void { /** @var array $form */ - $config = $this->configFactory->getEditable('analyze_ai_sentiments.settings'); - $sentiments = $config->get('sentiments') ?: []; + $values = $form_state->getValues(); // Get the maximum weight and add 1. + $sentiments = $this->sentimentsStorage->getAllSentiments(); $max_weight = 0; - foreach ($sentiments as $sentiments) { - $max_weight = max($max_weight, $sentiments['weight'] ?? 0); + foreach ($sentiments as $sentiment) { + $max_weight = max($max_weight, $sentiment['weight'] ?? 0); } - $values = $form_state->getValues(); - $sentiments[$values['id']] = [ - 'id' => $values['id'], - 'label' => $values['label'], - 'min_label' => $values['min_label'], - 'mid_label' => $values['mid_label'], - 'max_label' => $values['max_label'], - 'weight' => $max_weight + 1, - ]; + $this->sentimentsStorage->saveSentiment( + $values['id'], + $values['label'], + $values['min_label'], + $values['mid_label'], + $values['max_label'], + $max_weight + 1 + ); - $config->set('sentiments', $sentiments)->save(); - $this->messenger()->addStatus($this->t('Added new sentiments %label.', ['%label' => $values['label']])); + $this->messenger()->addStatus($this->t('Added new sentiment %label.', ['%label' => $values['label']])); $form_state->setRedirectUrl(Url::fromRoute('analyze_ai_sentiments.settings')); } diff --git a/src/Form/SentimentsSettingsForm.php b/src/Form/SentimentsSettingsForm.php index 591ca31..5635242 100644 --- a/src/Form/SentimentsSettingsForm.php +++ b/src/Form/SentimentsSettingsForm.php @@ -3,10 +3,8 @@ namespace Drupal\analyze_ai_sentiments\Form; use Drupal\Core\Url; -use Drupal\Core\Form\ConfigFormBase; +use Drupal\Core\Form\FormBase; use Drupal\Core\Form\FormStateInterface; -use Drupal\Core\Config\ConfigFactoryInterface; -use Drupal\Core\Config\TypedConfigManagerInterface; use Drupal\analyze_ai_sentiments\Service\SentimentsStorageService; use Symfony\Component\DependencyInjection\ContainerInterface; use Drupal\Core\Session\AccountProxyInterface; @@ -14,7 +12,7 @@ /** * Configure sentiments analysis settings. */ -class SentimentsSettingsForm extends ConfigFormBase { +class SentimentsSettingsForm extends FormBase { /** * The sentiments storage service. */ @@ -28,22 +26,15 @@ class SentimentsSettingsForm extends ConfigFormBase { /** * Constructs a SentimentsSettingsForm object. * - * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory - * The config factory. - * @param \Drupal\Core\Config\TypedConfigManagerInterface $typed_config_manager - * The typed config manager. * @param \Drupal\analyze_ai_sentiments\Service\SentimentsStorageService $sentiments_storage * The sentiments storage service. * @param \Drupal\Core\Session\AccountProxyInterface $current_user * The current user service. */ public function __construct( - ConfigFactoryInterface $config_factory, - TypedConfigManagerInterface $typed_config_manager, SentimentsStorageService $sentiments_storage, AccountProxyInterface $current_user, ) { - parent::__construct($config_factory, $typed_config_manager); $this->sentimentstorage = $sentiments_storage; $this->currentUser = $current_user; } @@ -54,8 +45,6 @@ public function __construct( public static function create(ContainerInterface $container): static { /** @var static */ return new self( - $container->get('config.factory'), - $container->get('config.typed'), $container->get('analyze_ai_sentiments.storage'), $container->get('current_user') ); @@ -68,67 +57,12 @@ public function getFormId() { return 'analyze_ai_sentiments_settings'; } - /** - * {@inheritdoc} - */ - protected function getEditableConfigNames(): array { - /** @var array */ - return ['analyze_ai_sentiments.settings']; - } - - /** - * Gets the default sentiments configurations. - * - * @return array> - * Array of default sentiments configurations. - */ - public function getDefaultSentiments(): array { - return [ - 'sentiments' => [ - 'label' => $this->t('Overall Sentiments'), - 'min_label' => $this->t('Negative'), - 'mid_label' => $this->t('Neutral'), - 'max_label' => $this->t('Positive'), - 'weight' => 0, - ], - 'engagement' => [ - 'label' => $this->t('Engagement Level'), - 'min_label' => $this->t('Passive'), - 'mid_label' => $this->t('Balanced'), - 'max_label' => $this->t('Interactive'), - 'weight' => 1, - ], - 'trust' => [ - 'label' => $this->t('Trust/Credibility'), - 'min_label' => $this->t('Promotional'), - 'mid_label' => $this->t('Balanced'), - 'max_label' => $this->t('Authoritative'), - 'weight' => 2, - ], - 'objectivity' => [ - 'label' => $this->t('Objectivity'), - 'min_label' => $this->t('Subjective'), - 'mid_label' => $this->t('Mixed'), - 'max_label' => $this->t('Objective'), - 'weight' => 3, - ], - 'complexity' => [ - 'label' => $this->t('Technical Complexity'), - 'min_label' => $this->t('Basic'), - 'mid_label' => $this->t('Moderate'), - 'max_label' => $this->t('Complex'), - 'weight' => 4, - ], - ]; - } - /** * {@inheritdoc} */ public function buildForm(array $form, FormStateInterface $form_state): array { /** @var array $form */ - $config = $this->config('analyze_ai_sentiments.settings'); - $sentiments = $config->get('sentiments') ?: $this->getDefaultSentiments(); + $sentiments = $this->sentimentstorage->getAllSentiments(); $form['description'] = [ '#type' => 'html_tag', @@ -166,7 +100,7 @@ public function buildForm(array $form, FormStateInterface $form_state): array { '#attributes' => ['class' => ['sentiments-table-container']], ]; - $form['table']['sentiments'] = [ + $form['sentiments'] = [ '#type' => 'table', '#header' => [ $this->t('Sentiments'), @@ -189,8 +123,12 @@ public function buildForm(array $form, FormStateInterface $form_state): array { }); // Add existing sentiments to the table. - foreach ($sentiments as $id => $sentiments) { - $form['table']['sentiments'][$id] = [ + foreach ($sentiments as $id => $sentiment) { + // Add safety check for corrupted data. + if (!is_array($sentiment) || !isset($sentiment['label'])) { + continue; + } + $form['sentiments'][$id] = [ '#attributes' => [ 'class' => ['draggable'], ], @@ -198,7 +136,7 @@ public function buildForm(array $form, FormStateInterface $form_state): array { '#type' => 'textfield', '#title' => $this->t('Label'), '#title_display' => 'invisible', - '#default_value' => $sentiments['label'], + '#default_value' => $sentiment['label'], '#required' => TRUE, ], 'labels' => [ @@ -207,19 +145,19 @@ public function buildForm(array $form, FormStateInterface $form_state): array { 'min_label' => [ '#type' => 'textfield', '#title' => $this->t('Minimum'), - '#default_value' => $sentiments['min_label'], + '#default_value' => $sentiment['min_label'], '#required' => TRUE, ], 'mid_label' => [ '#type' => 'textfield', '#title' => $this->t('Middle'), - '#default_value' => $sentiments['mid_label'], + '#default_value' => $sentiment['mid_label'], '#required' => TRUE, ], 'max_label' => [ '#type' => 'textfield', '#title' => $this->t('Maximum'), - '#default_value' => $sentiments['max_label'], + '#default_value' => $sentiment['max_label'], '#required' => TRUE, ], ], @@ -227,7 +165,7 @@ public function buildForm(array $form, FormStateInterface $form_state): array { '#type' => 'weight', '#title' => $this->t('Weight'), '#title_display' => 'invisible', - '#default_value' => $sentiments['weight'], + '#default_value' => $sentiment['weight'], '#attributes' => ['class' => ['sentiments-weight']], ], 'operations' => [ @@ -256,11 +194,15 @@ public function buildForm(array $form, FormStateInterface $form_state): array { ]; } - $form = parent::buildForm($form, $form_state); - - // Improve the save button. - $form['actions']['submit']['#value'] = $this->t('Save changes'); - $form['actions']['submit']['#attributes']['class'][] = 'button--primary'; + // Add form actions. + $form['actions'] = [ + '#type' => 'actions', + 'submit' => [ + '#type' => 'submit', + '#value' => $this->t('Save changes'), + '#button_type' => 'primary', + ], + ]; return $form; } @@ -270,26 +212,25 @@ public function buildForm(array $form, FormStateInterface $form_state): array { */ public function submitForm(array &$form, FormStateInterface $form_state): void { /** @var array $form */ - $sentiments = []; - foreach ($form_state->getValue('sentiments') as $id => $values) { - $sentiments[$id] = [ - 'label' => $values['label'], - 'min_label' => $values['labels']['min_label'], - 'mid_label' => $values['labels']['mid_label'], - 'max_label' => $values['labels']['max_label'], - 'weight' => $values['weight'], - ]; - } + // Save each sentiment using the storage service. + $sentiments_to_save = $form_state->getValue('sentiments') ?: []; - $this->config('analyze_ai_sentiments.settings') - ->set('sentiments', $sentiments) - ->save(); + foreach ($sentiments_to_save as $id => $values) { + if (!isset($values['labels']['min_label'])) { + continue; + } - // Invalidate all cached sentiments analysis results since configuration - // changed. - $this->sentimentstorage->invalidateConfigCache(); + $this->sentimentstorage->saveSentiment( + $id, + $values['label'], + $values['labels']['min_label'], + $values['labels']['mid_label'], + $values['labels']['max_label'], + (int) $values['weight'] + ); + } - parent::submitForm($form, $form_state); + $this->messenger()->addStatus($this->t('The configuration options have been saved.')); } } diff --git a/src/Plugin/Analyze/AISentimentsAnalyzer.php b/src/Plugin/Analyze/AISentimentsAnalyzer.php index 300cde8..1500a7c 100644 --- a/src/Plugin/Analyze/AISentimentsAnalyzer.php +++ b/src/Plugin/Analyze/AISentimentsAnalyzer.php @@ -144,32 +144,7 @@ public static function create(ContainerInterface $container, array $configuratio * Array of sentiments configurations. */ protected function getConfiguredSentiments(): array { - $config = $this->configFactory->get('analyze_ai_sentiments.settings'); - $sentiments = $config->get('sentiments'); - - if (empty($sentiments)) { - // Load defaults if no sentiments configured. - return [ - 'trust' => [ - 'id' => 'trust', - 'label' => 'Trust/Credibility', - 'min_label' => 'Promotional', - 'mid_label' => 'Balanced', - 'max_label' => 'Authoritative', - 'weight' => 0, - ], - 'objectivity' => [ - 'id' => 'objectivity', - 'label' => 'Objectivity', - 'min_label' => 'Subjective', - 'mid_label' => 'Mixed', - 'max_label' => 'Objective', - 'weight' => 1, - ], - ]; - } - - return $sentiments; + return $this->storage->getAllSentiments(); } /** @@ -197,14 +172,14 @@ protected function getEnabledSentiments(string $entity_type_id, ?string $bundle $sentiments = $this->getConfiguredSentiments(); $enabled = []; - foreach ($sentiments as $id => $sentiments) { + foreach ($sentiments as $id => $sentiment) { // If no settings exist yet, enable all sentiments by default. if (!isset($settings['sentiments'])) { - $enabled[$id] = $sentiments; + $enabled[$id] = $sentiment; } // Otherwise check if explicitly enabled in settings. elseif (isset($settings['sentiments'][$id]) && $settings['sentiments'][$id]) { - $enabled[$id] = $sentiments; + $enabled[$id] = $sentiment; } } @@ -370,17 +345,17 @@ public function renderFullReport(EntityInterface $entity): array { ], ]; - foreach ($enabled_sentiments as $id => $sentiments) { + foreach ($enabled_sentiments as $id => $sentiment) { if (isset($scores[$id])) { // Convert -1 to +1 range to 0 to 1 for gauge. $gauge_value = ($scores[$id] + 1) / 2; $build[$id] = [ '#theme' => 'analyze_gauge', - '#caption' => $this->t('@label', ['@label' => $sentiments['label']]), - '#range_min_label' => $this->t('@label', ['@label' => $sentiments['min_label']]), - '#range_mid_label' => $this->t('@label', ['@label' => $sentiments['mid_label']]), - '#range_max_label' => $this->t('@label', ['@label' => $sentiments['max_label']]), + '#caption' => $this->t('@label', ['@label' => $sentiment['label']]), + '#range_min_label' => $this->t('@label', ['@label' => $sentiment['min_label']]), + '#range_mid_label' => $this->t('@label', ['@label' => $sentiment['mid_label']]), + '#range_max_label' => $this->t('@label', ['@label' => $sentiment['max_label']]), '#range_min' => -1, '#range_max' => 1, '#value' => $gauge_value, @@ -391,7 +366,7 @@ public function renderFullReport(EntityInterface $entity): array { // Show analysis failure message for sentiments without scores. $build[$id] = [ '#theme' => 'analyze_table', - '#table_title' => $sentiments['label'], + '#table_title' => $sentiment['label'], '#rows' => [ [ 'label' => 'Status', @@ -467,13 +442,13 @@ protected function analyzeSentiments(EntityInterface $entity): array { // Build sentiments descriptions with their ranges. $sentiments_descriptions = []; - foreach ($enabled_sentiments as $id => $sentiments) { + foreach ($enabled_sentiments as $id => $sentiment) { $sentiments_descriptions[] = sprintf( "- %s: Score from -1.0 (%s) to +1.0 (%s), with 0.0 being %s", - $sentiments['label'], - $sentiments['min_label'], - $sentiments['max_label'], - $sentiments['mid_label'] + $sentiment['label'], + $sentiment['min_label'], + $sentiment['max_label'], + $sentiment['mid_label'] ); } @@ -519,7 +494,7 @@ protected function analyzeSentiments(EntityInterface $entity): array { // Validate and normalize scores to ensure they're within -1 to +1 range. $scores = []; - foreach ($enabled_sentiments as $id => $sentiments) { + foreach ($enabled_sentiments as $id => $sentiment) { if (isset($decoded[$id])) { $score = (float) $decoded[$id]; // Clamp score to -1 to +1 range. @@ -573,7 +548,7 @@ public function getDefaultSettings(): array { $sentiments = $this->getConfiguredSentiments(); $default_sentiments = []; - foreach ($sentiments as $id => $sentiments) { + foreach ($sentiments as $id => $sentiment) { $default_sentiments[$id] = TRUE; } @@ -597,10 +572,10 @@ public function getConfigurableSettings(): array { $sentiments = $this->getConfiguredSentiments(); $settings = []; - foreach ($sentiments as $id => $sentiments) { + foreach ($sentiments as $id => $sentiment) { $settings[$id] = [ 'type' => 'checkbox', - 'title' => $sentiments['label'], + 'title' => $sentiment['label'], 'default_value' => TRUE, ]; } diff --git a/src/Service/SentimentsStorageService.php b/src/Service/SentimentsStorageService.php index 5d14837..cc1dd59 100644 --- a/src/Service/SentimentsStorageService.php +++ b/src/Service/SentimentsStorageService.php @@ -227,4 +227,170 @@ private function getEntityContent(EntityInterface $entity): string { return $content; } + /** + * Gets all configured sentiments. + * + * @return array> + * Array of sentiment configurations keyed by sentiment ID. + */ + public function getAllSentiments(): array { + $config = $this->configFactory->get('analyze_ai_sentiments.settings'); + $sentiments = $config->get('sentiments'); + + // If no sentiments configured, return defaults. + if (empty($sentiments)) { + return $this->getDefaultSentiments(); + } + + // Validate structure - each value should be an array with required keys. + $valid_sentiments = []; + foreach ($sentiments as $id => $sentiment) { + if (is_array($sentiment) && isset($sentiment['label'], $sentiment['min_label'], $sentiment['mid_label'], $sentiment['max_label'])) { + $valid_sentiments[$id] = $sentiment; + } + } + + // If no valid sentiments, return defaults. + if (empty($valid_sentiments)) { + return $this->getDefaultSentiments(); + } + + return $valid_sentiments; + } + + /** + * Gets a specific sentiment by ID. + * + * @param string $sentiment_id + * The sentiment ID. + * + * @return array|null + * The sentiment configuration or NULL if not found. + */ + public function getSentiment(string $sentiment_id): ?array { + $sentiments = $this->getAllSentiments(); + return $sentiments[$sentiment_id] ?? NULL; + } + + /** + * Saves a sentiment configuration. + * + * @param string $sentiment_id + * The sentiment ID. + * @param string $label + * The sentiment label. + * @param string $min_label + * The minimum range label. + * @param string $mid_label + * The middle range label. + * @param string $max_label + * The maximum range label. + * @param int $weight + * The sentiment weight for ordering. + */ + public function saveSentiment( + string $sentiment_id, + string $label, + string $min_label, + string $mid_label, + string $max_label, + int $weight, + ): void { + $config = $this->configFactory->getEditable('analyze_ai_sentiments.settings'); + $existing_sentiments = $config->get('sentiments') ?: []; + + $existing_sentiments[$sentiment_id] = [ + 'id' => $sentiment_id, + 'label' => $label, + 'min_label' => $min_label, + 'mid_label' => $mid_label, + 'max_label' => $max_label, + 'weight' => $weight, + ]; + + $config->set('sentiments', $existing_sentiments)->save(); + $this->invalidateConfigCache(); + } + + /** + * Deletes a sentiment configuration. + * + * @param string $sentiment_id + * The sentiment ID to delete. + */ + public function deleteSentiment(string $sentiment_id): void { + $config = $this->configFactory->getEditable('analyze_ai_sentiments.settings'); + $sentiments = $config->get('sentiments') ?: []; + + if (isset($sentiments[$sentiment_id])) { + unset($sentiments[$sentiment_id]); + $config->set('sentiments', $sentiments)->save(); + $this->invalidateConfigCache(); + } + } + + /** + * Checks if a sentiment ID exists. + * + * @param string $sentiment_id + * The sentiment ID to check. + * + * @return bool + * TRUE if the sentiment exists, FALSE otherwise. + */ + public function sentimentExists(string $sentiment_id): bool { + return $this->getSentiment($sentiment_id) !== NULL; + } + + /** + * Gets the default sentiment configurations. + * + * @return array> + * Array of default sentiment configurations. + */ + private function getDefaultSentiments(): array { + return [ + 'overall' => [ + 'id' => 'overall', + 'label' => 'Overall Sentiments', + 'min_label' => 'Negative', + 'mid_label' => 'Neutral', + 'max_label' => 'Positive', + 'weight' => 0, + ], + 'engagement' => [ + 'id' => 'engagement', + 'label' => 'Engagement Level', + 'min_label' => 'Passive', + 'mid_label' => 'Balanced', + 'max_label' => 'Interactive', + 'weight' => 1, + ], + 'trust' => [ + 'id' => 'trust', + 'label' => 'Trust/Credibility', + 'min_label' => 'Promotional', + 'mid_label' => 'Balanced', + 'max_label' => 'Authoritative', + 'weight' => 2, + ], + 'objectivity' => [ + 'id' => 'objectivity', + 'label' => 'Objectivity', + 'min_label' => 'Subjective', + 'mid_label' => 'Mixed', + 'max_label' => 'Objective', + 'weight' => 3, + ], + 'complexity' => [ + 'id' => 'complexity', + 'label' => 'Technical Complexity', + 'min_label' => 'Basic', + 'mid_label' => 'Moderate', + 'max_label' => 'Complex', + 'weight' => 4, + ], + ]; + } + }