Skip to content

Commit

Permalink
Merge pull request #957 from UN-OCHA/feature/RW-1134
Browse files Browse the repository at this point in the history
[RW-1134] Show chat popup to anonymous users
  • Loading branch information
orakili authored Dec 6, 2024
2 parents e19ea14 + 2f6f288 commit fd4d4de
Show file tree
Hide file tree
Showing 12 changed files with 321 additions and 91 deletions.
1 change: 1 addition & 0 deletions config/core.extension.yml
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ module:
path: 0
path_alias: 0
redirect: 0
reliefweb_ai: 0
reliefweb_analytics: 0
reliefweb_api: 0
reliefweb_bookmarks: 0
Expand Down
8 changes: 8 additions & 0 deletions config/reliefweb_ai.settings.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
_core:
default_config_hash: fU8Wp0OCsudl5x0QSV_mUWWt2KbQuzhVwEB-o9A8peg
ocha_ai_chat:
allow_for_anonymous: false
instructions_replace: false
login_instructions: |
<p>The use of <strong>Ask ReliefWeb</strong> requires an account on ReliefWeb.</p>
<p>Please <a href="/user/login/hid?destination=@destination" target="_parent">login</a> or <a href="https://auth.humanitarian.id/register" target="_parent">create a new account</a>.</p>
2 changes: 2 additions & 0 deletions config/user.role.anonymous.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ status: true
dependencies:
module:
- media
- ocha_ai_chat
- system
_core:
default_config_hash: j5zLMOdJBqC0bMvSdth5UebkprJB8g_2FXHqhfpJzow
Expand All @@ -13,4 +14,5 @@ weight: 0
is_admin: false
permissions:
- 'access content'
- 'access ocha ai chat'
- 'view media'
8 changes: 8 additions & 0 deletions html/modules/custom/reliefweb_ai/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
ReliefWeb AI
============

AI usage for ReliefWeb.

## AI chat

Currently this module simply alters the chat form to display the chat popup to anonymous users but asking them to log in or register an account to use the chat. This behavior can be controller via the configuration of this module.
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
ocha_ai_chat:
allow_for_anonymous: false
instructions_replace: false
login_instructions: |
<p>The use of <strong>Ask ReliefWeb</strong> requires an account on ReliefWeb.</p>
<p>Please <a href="/user/login/hid?destination=@destination" target="_parent">login</a> or <a href="https://auth.humanitarian.id/register" target="_parent">create a new account</a>.</p>
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
reliefweb_ai.settings:
type: config_object
label: 'ReliefWeb AI settings.'
mapping:
ocha_ai_chat:
type: mapping
label: 'Settings for the OCHA AI chat.'
mapping:
allow_for_anonymous:
type: boolean
label: 'Allow anonymous user to access the chat.'
instructions_replace:
type: boolean
label: 'If TRUE, replace the chat instructions when the chat is disabled (error, anonymous access etc.), otherwise append the extra instructions.'
login_instructions:
type: text
label: 'Login or register instructions for anonymous users.'

7 changes: 7 additions & 0 deletions html/modules/custom/reliefweb_ai/reliefweb_ai.info.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
type: module
name: ReliefWeb AI
description: 'AI usage for ReliefWeb'
package: reliefweb
core_version_requirement: ^10
dependencies:
- drupal:ocha_ai
154 changes: 154 additions & 0 deletions html/modules/custom/reliefweb_ai/reliefweb_ai.module
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
<?php

/**
* @file
* ReliefWeb AI module file.
*/

use Drupal\Component\Utility\UrlHelper;
use Drupal\Core\Block\BlockPluginInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Render\Element;
use Drupal\reliefweb_ai\OchaAiChatPopupBlockHandler;

/**
* Implements hook_form_FORM_ID_alter() for `ocha_ai_chat_chat_form`.
*/
function reliefweb_ai_form_ocha_ai_chat_chat_form_alter(array &$form, FormStateInterface $form_state, string $form_id) {
$config = \Drupal::config('reliefweb_ai.settings');
$current_user = \Drupal::currentUser();
$url = \Drupal::request()?->query?->get('url');

// Add some caching context and tags.
$form['#cache']['contexts'] = array_merge($form['#cache']['contexts'] ?? [], [
'user.roles', 'url.query_args',
]);
$form['#cache']['tags'] = array_merge($form['#cache']['tags'] ?? [], [
'config:reliefweb_ai.settings',
]);

// Add a more unique class to the chat submit button.
if (isset($form['actions']['submit'])) {
$form['actions']['submit']['#attributes']['class'][] = 'ocha-ai-chat-ask';
}

// Message to display if the form is disabled for any reason.
$disabled = '';

// Check if the user is anonymous and not allowed to access the chat.
if ($current_user->isAnonymous() && !$config->get('ocha_ai_chat.allow_for_anonymous')) {
$disabled = $config->get('ocha_ai_chat.login_instructions') ?? '';

// Redirect to the current page if possible.
if (!empty($url)) {
$disabled = strtr($disabled, [
'@destination' => UrlHelper::encodePath(parse_url($url, \PHP_URL_PATH)),
]);
}
}

if (empty($disabled)) {
// Check if we have a URL to allow the chat.
if (empty($url)) {
$instructions = t('<p>Something went wrong.</p>');
}
// Otherwise check if the language or type of the document.
else {
$router = \Drupal::service('router.no_access_checks');
$parameters = $router->match($url);
$node = $parameters['node'] ?? NULL;

// Disable the form if it's not a report.
if (!isset($node) || $node->bundle() !== 'report') {
$disabled = t('<p>Something went wrong.</p>');
}
else {
// No need to show the source when chatting with a single report.
if (isset($form['source'])) {
$form['source']['#access'] = FALSE;
}

// Only English documents are supported due to LLM limitations.
$is_english_report = FALSE;
foreach ($node->field_language as $item) {
if ($item->target_id == 267) {
$is_english_report = TRUE;
break;
}
}
if (!$is_english_report) {
$disabled = t('<p>Sorry, only <strong>English</strong> reports are supported.</p>');
}

// Non supported content formats.
foreach ($node->field_content_format as $item) {
if ($item->target_id == 12) {
$disabled = t('<p>Sorry, <strong>maps</strong> are not supported.</p>');
break;
}
elseif ($item->target_id == 12570) {
$disabled = t('<p>Sorry, <strong>infographics</strong> are not supported.</p>');
break;
}
elseif ($item->target_id == 38974) {
$disabled = t('<p>Sorry, <strong>interactive reports</strong> are not supported.</p>');
break;
}
}
}
}
}

if (!empty($disabled)) {
// Check whether we are requested to replace the instructions or append the
// disabled instructions.
$replace = $config->get('ocha_ai_chat.instructions_replace') === TRUE || !isset($form['chat']['content']);

if (!$replace) {
// We cannot just append the instructions to the current ones because of
// the text format may include sanitation that removes the target
// attributes. We indeed need to preserve those attributes in the login
// instructions so the login and register links open in parent window
// and not in the chat iframe.
// So first we format the current instructions and then append the extra
// instructions.
$text = $form['chat']['content']['#text'] ?? '';
$format = $form['chat']['content']['#format'] ?? 'markdown_editor';
$instructions = (string) check_markup($text, $format);
$disabled = $instructions . $disabled;
}

// Replace the instructions with a simple markup render element.
$form['chat']['content'] = [
'#type' => 'markup',
'#markup' => $disabled,
'#prefix' => '<div id="ocha-ai-chat-instructions" class="ocha-ai-chat-chat-form__instructions">',
'#suffix' => '</div>',
];

// Disable or hide the reset of the form.
foreach (Element::children($form['chat']) as $key) {
if ($key !== 'content') {
$form['chat'][$key]['#access'] = FALSE;
}
}
foreach (Element::children($form) as $key) {
if ($key !== 'chat') {
$form[$key]['#disabled'] = TRUE;
}
}

$form['#cache']['max-age'] = 3600;
}
}

/**
* Implements hook_block_view_alter().
*/
function reliefweb_ai_block_view_alter(array &$build, BlockPluginInterface $block): void {
// Alter the chat popup block, notably to adjust the caching.
if ($block->getPluginId() === 'ocha_ai_chat_chat_popup') {
$build['#pre_render'][] = [OchaAiChatPopupBlockHandler::class, 'alterBuild'];
return;
}
}
6 changes: 6 additions & 0 deletions html/modules/custom/reliefweb_ai/reliefweb_ai.services.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
services:
reliefweb_ai.ocha_ai_chat_cache_subscriber:
class: Drupal\reliefweb_ai\EventSubscriber\OchaAiChatCacheSubscriber
arguments: ['@config.factory', '@current_user']
tags:
- { name: event_subscriber }
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
<?php

namespace Drupal\reliefweb_ai\EventSubscriber;

use Drupal\Core\Cache\CacheableResponseInterface;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Session\AccountProxyInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpKernel\Event\ResponseEvent;
use Symfony\Component\HttpKernel\KernelEvents;

/**
* Modify caching of the OCHA AI chat form.
*/
class OchaAiChatCacheSubscriber implements EventSubscriberInterface {

/**
* Constructor.
*
* @param \Drupal\Core\Config\ConfigFactoryInterface $configFactory
* The config factory.
* @param \Drupal\Core\Session\AccountProxyInterface $currentUser
* The current user.
*/
public function __construct(
protected ConfigFactoryInterface $configFactory,
protected AccountProxyInterface $currentUser,
) {}

/**
* {@inheritdoc}
*/
public static function getSubscribedEvents(): array {
return [
// We need to run last to ensure the cache is not overridden.
KernelEvents::RESPONSE => ['onResponse', -1000],
];
}

/**
* {@inheritdoc}
*/
public function onResponse(ResponseEvent $event): void {
$request = $event->getRequest();
$route = $request->attributes->get('_route');

if ($route === 'ocha_ai_chat.chat_form' || $route === 'ocha_ai_chat.chat_form.popup') {
$response = $event->getResponse();

// Ajax response are not cacheable so only handle normal form response.
if ($response instanceof CacheableResponseInterface) {
$config = $this->configFactory->get('reliefweb_ai.settings');

$cache_metadata = $response->getCacheableMetadata();

// Vary the cache by role, url parameters and config since they control
// what is displayed to the user.
$cache_metadata->addCacheContexts(['user.roles', 'url.query_args']);
$cache_metadata->addCacheTags(['config:reliefweb_ai.settings']);

// Cache the response for 1 hour for anonymous user when we just show
// a disabled form asking to log in or register.
if ($this->currentUser->isAnonymous() && !$config->get('ocha_ai_chat.allow_for_anonymous')) {
// Cache for 1 hour.
$cache_metadata->setCacheMaxAge(3600);
// Ensure varnish for example can cache the page.
$response->headers->set('Cache-Control', 'public, max-age=3600');
}
}
}
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
<?php

declare(strict_types=1);

namespace Drupal\reliefweb_ai;

use Drupal\Core\Security\TrustedCallbackInterface;

/**
* Handle modification to the OCHA AI chat popup block.
*/
class OchaAiChatPopupBlockHandler implements TrustedCallbackInterface {

/**
* {@inheritdoc}
*/
public static function trustedCallbacks(): array {
return ['alterBuild'];
}

/**
* Alter the block build.
*
* @param array $build
* Block build.
*
* @return array
* Modified block build.
*/
public static function alterBuild(array $build): array {
unset($build['content']['#cache']['max-age']);
$build['content']['#cache']['contexts'][] = 'user.roles';
$build['content']['#cache']['contexts'][] = 'url.path';
return $build;
}

}
Loading

0 comments on commit fd4d4de

Please sign in to comment.