From ac1161ad1091d6951c5a7dbfb0e565c7f3a3483a Mon Sep 17 00:00:00 2001 From: Julien Veyssier Date: Thu, 3 Aug 2023 14:18:19 +0200 Subject: [PATCH] implement notifications and custom handling in the web UI (open results in a modal) Signed-off-by: Julien Veyssier --- appinfo/routes.php | 2 +- lib/AppInfo/Application.php | 12 +-- lib/Controller/AssistantController.php | 5 +- .../BeforeTemplateRenderedListener.php | 39 ++++++++ lib/Listener/TaskFailedListener.php | 4 + lib/Listener/TaskSuccessfulListener.php | 4 + lib/Notification/Notifier.php | 95 +++++++++++++++++++ lib/Service/AssistantService.php | 33 +++++-- src/assistant.js | 67 +++++++++++-- 9 files changed, 237 insertions(+), 24 deletions(-) create mode 100644 lib/Listener/BeforeTemplateRenderedListener.php create mode 100644 lib/Notification/Notifier.php diff --git a/appinfo/routes.php b/appinfo/routes.php index 230870d2..535cb795 100644 --- a/appinfo/routes.php +++ b/appinfo/routes.php @@ -6,6 +6,6 @@ ['name' => 'config#setAdminConfig', 'url' => '/admin-config', 'verb' => 'PUT'], ['name' => 'assistant#getplop', 'url' => '/plop', 'verb' => 'GET'], - ['name' => 'assistant#getTaskResultPage', 'url' => '/t/{id}', 'verb' => 'GET'], + ['name' => 'assistant#getTaskResultPage', 'url' => '/t/{taskId}', 'verb' => 'GET'], ], ]; diff --git a/lib/AppInfo/Application.php b/lib/AppInfo/Application.php index 7ee55464..ab13730c 100644 --- a/lib/AppInfo/Application.php +++ b/lib/AppInfo/Application.php @@ -2,8 +2,11 @@ namespace OCA\TPAssistant\AppInfo; +use OCA\TPAssistant\Listener\BeforeTemplateRenderedListener; use OCA\TPAssistant\Listener\TaskFailedListener; use OCA\TPAssistant\Listener\TaskSuccessfulListener; +use OCA\TPAssistant\Notification\Notifier; +use OCP\AppFramework\Http\Events\BeforeTemplateRenderedEvent; use OCP\IConfig; use OCP\AppFramework\App; @@ -12,7 +15,6 @@ use OCP\AppFramework\Bootstrap\IBootstrap; use OCP\TextProcessing\Events\TaskFailedEvent; use OCP\TextProcessing\Events\TaskSuccessfulEvent; -use OCP\Util; class Application extends App implements IBootstrap { @@ -28,16 +30,14 @@ public function __construct(array $urlParams = []) { } public function register(IRegistrationContext $context): void { + $context->registerEventListener(BeforeTemplateRenderedEvent::class, BeforeTemplateRenderedListener::class); + $context->registerEventListener(TaskSuccessfulEvent::class, TaskSuccessfulListener::class); $context->registerEventListener(TaskFailedEvent::class, TaskFailedListener::class); + $context->registerNotifierService(Notifier::class); } public function boot(IBootContext $context): void { - $context->injectFn(function ( - $userId - ) { - Util::addScript(self::APP_ID, self::APP_ID . '-assistant'); - }); } } diff --git a/lib/Controller/AssistantController.php b/lib/Controller/AssistantController.php index 5e836a0d..19c06b4b 100644 --- a/lib/Controller/AssistantController.php +++ b/lib/Controller/AssistantController.php @@ -38,9 +38,8 @@ public function __construct( */ #[NoAdminRequired] #[NoCSRFRequired] - public function getImageGenerationPage(string $hash): TemplateResponse { - $generationData = $this->assistantService->getGenerationInfo($hash); - $this->initialStateService->provideInitialState('generation', $generationData); + public function getTaskResultPage(int $taskId): TemplateResponse { + $this->initialStateService->provideInitialState('taskId', $taskId); return new TemplateResponse(Application::APP_ID, 'taskResultPage'); } } diff --git a/lib/Listener/BeforeTemplateRenderedListener.php b/lib/Listener/BeforeTemplateRenderedListener.php new file mode 100644 index 00000000..80b5eed1 --- /dev/null +++ b/lib/Listener/BeforeTemplateRenderedListener.php @@ -0,0 +1,39 @@ +getResponse()->getRenderAs() !== TemplateResponse::RENDER_AS_USER) { + return; + } + + if (!$this->userSession->getUser() instanceof IUser) { + return; + } + + Util::addScript(Application::APP_ID, Application::APP_ID . '-assistant'); + } +} diff --git a/lib/Listener/TaskFailedListener.php b/lib/Listener/TaskFailedListener.php index 826151dd..86e16176 100644 --- a/lib/Listener/TaskFailedListener.php +++ b/lib/Listener/TaskFailedListener.php @@ -2,6 +2,7 @@ namespace OCA\TPAssistant\Listener; +use OCA\TPAssistant\Service\AssistantService; use OCP\EventDispatcher\Event; use OCP\EventDispatcher\IEventListener; use OCP\TextProcessing\Events\TaskFailedEvent; @@ -9,6 +10,7 @@ class TaskFailedListener implements IEventListener { public function __construct( + private AssistantService $assistantService, ) { } @@ -17,6 +19,8 @@ public function handle(Event $event): void { return; } + $task = $event->getTask(); + $this->assistantService->sendNotification($task); error_log('Task failed'); } } diff --git a/lib/Listener/TaskSuccessfulListener.php b/lib/Listener/TaskSuccessfulListener.php index 05267fcf..87f673a6 100644 --- a/lib/Listener/TaskSuccessfulListener.php +++ b/lib/Listener/TaskSuccessfulListener.php @@ -2,6 +2,7 @@ namespace OCA\TPAssistant\Listener; +use OCA\TPAssistant\Service\AssistantService; use OCP\EventDispatcher\Event; use OCP\EventDispatcher\IEventListener; use OCP\TextProcessing\Events\TaskSuccessfulEvent; @@ -9,6 +10,7 @@ class TaskSuccessfulListener implements IEventListener { public function __construct( + private AssistantService $assistantService, ) { } @@ -17,6 +19,8 @@ public function handle(Event $event): void { return; } + $task = $event->getTask(); + $this->assistantService->sendNotification($task); error_log('Task successful'); } } diff --git a/lib/Notification/Notifier.php b/lib/Notification/Notifier.php new file mode 100644 index 00000000..f76c86c0 --- /dev/null +++ b/lib/Notification/Notifier.php @@ -0,0 +1,95 @@ +factory->get(Application::APP_ID)->t('Nextcloud assistant'); + } + + /** + * @param INotification $notification + * @param string $languageCode The code of the language that should be used to prepare the notification + * @return INotification + * @throws InvalidArgumentException When the notification was not prepared by a notifier + * @since 9.0.0 + */ + public function prepare(INotification $notification, string $languageCode): INotification { + if ($notification->getApp() !== Application::APP_ID) { + // Not my app => throw + throw new InvalidArgumentException(); + } + + $l = $this->factory->get(Application::APP_ID, $languageCode); + + $params = $notification->getSubjectParameters(); + $iconUrl = $this->url->getAbsoluteURL($this->url->imagePath(Application::APP_ID, 'app-dark.svg')); + + switch ($notification->getSubject()) { + case 'success': + + $subject = $l->t('Assistant Task for app %1$s has finished', [$params['appId']]); + $content = $l->t('The input was: %1$s', [$params['input']]); + $link = $this->url->linkToRouteAbsolute(Application::APP_ID . '.assistant.getTaskResultPage', ['taskId' => $params['id']]); + + $notification + ->setParsedSubject($subject) + ->setParsedMessage($content) + ->setLink($link) + ->setIcon($iconUrl); + + $actionLabel = $l->t('View results'); + $action = $notification->createAction(); + $action->setLabel($actionLabel) + ->setParsedLabel($actionLabel) + ->setLink($notification->getLink(), IAction::TYPE_WEB) + ->setPrimary(true); + + $notification->addParsedAction($action); + + return $notification; + + case 'failure': + return $notification; + + default: + // Unknown subject => Unknown notification => throw + throw new InvalidArgumentException(); + } + } +} diff --git a/lib/Service/AssistantService.php b/lib/Service/AssistantService.php index 9c1d0e1e..62aba488 100644 --- a/lib/Service/AssistantService.php +++ b/lib/Service/AssistantService.php @@ -2,18 +2,39 @@ namespace OCA\TPAssistant\Service; -use OCP\IConfig; -use OCP\IL10N; -use Psr\Log\LoggerInterface; +use DateTime; +use OCA\TPAssistant\AppInfo\Application; +use OCP\TextProcessing\Task; +use OCP\Notification\IManager as INotificationManager; class AssistantService { public function __construct( string $appName, - private LoggerInterface $logger, - private IL10N $l10n, - private IConfig $config, + private INotificationManager $notificationManager, ) { } + public function sendNotification(Task $task): void { + $manager = $this->notificationManager; + $notification = $manager->createNotification(); + + $params = [ + 'appId' => $task->getAppId(), + 'id' => $task->getId(), + 'input' => $task->getInput(), + ]; + $status = $task->getStatus(); + $subject = $status === Task::STATUS_SUCCESSFUL + ? 'success' + : 'failure'; + $notification->setApp(Application::APP_ID) + ->setUser($task->getUserId()) + ->setDateTime(new DateTime()) + ->setObject('task', $task->getId()) + ->setSubject($subject, $params); + + $manager->notify($notification); + } + } diff --git a/src/assistant.js b/src/assistant.js index d608c670..cabeb312 100644 --- a/src/assistant.js +++ b/src/assistant.js @@ -3,13 +3,10 @@ import { getRequestToken } from '@nextcloud/auth' __webpack_nonce__ = btoa(getRequestToken()) // eslint-disable-line __webpack_public_path__ = linkTo('textprocessing_assistant', 'js/') // eslint-disable-line -/** - * Creates an assistant modal and return a promise which provides the result - * - * @param {boolean} isInsideViewer Should be true if this function is called while the Viewer is displayed - * @return {Promise} - */ -export async function openAssistant(appId, identifier = '', taskType = null, inputText = '', isInsideViewer = undefined) { +// Creates an assistant modal and return a promise which provides the result +// TODO jsdoc +// OCA.TPAssistant.openTextProcessingModal('app1', 'IDID', 'OCP\\TextProcessing\\SummaryTaskType', 'megainput').then(r => {console.debug('yeyeyeyeyeye', r)}) +export async function openTextProcessingModal(appId, identifier = '', taskType = null, inputText = '', isInsideViewer = undefined) { const { default: Vue } = await import(/* webpackChunkName: "vue-lazy" */'vue') const { default: AssistantModal } = await import(/* webpackChunkName: "assistant-modal-lazy" */'./components/AssistantModal.vue') Vue.mixin({ methods: { t, n } }) @@ -61,15 +58,69 @@ async function scheduleTask({ appId, identifier, taskType, input }) { return axios.post(url, params) } +function handleNotification(event) { + console.debug('aaaaaa -------- handle notification', event) + if (event.notification.app !== 'textprocessing_assistant' || event.action.type !== 'WEB') { + return + } + event.cancelAction = true + showResults(event.notification.objectId) +} + +async function subscribeToNotifications() { + const { subscribe } = await import(/* webpackChunkName: "router-lazy" */'@nextcloud/event-bus') + subscribe('notifications:action:execute', handleNotification) + console.debug('aaaaa i subscribed', subscribe) +} + +async function showResults(taskId) { + const { default: axios } = await import(/* webpackChunkName: "axios-lazy" */'@nextcloud/axios') + const { generateOcsUrl } = await import(/* webpackChunkName: "router-lazy" */'@nextcloud/router') + const url = generateOcsUrl('textprocessing/task/{taskId}', { taskId }) + axios.get(url).then(response => { + console.debug('aaaaaa result of task', response.data) + openResultModal(response.data.ocs.data.task) + }).catch(error => { + console.error(error) + }) +} + +async function openResultModal(task) { + const { default: Vue } = await import(/* webpackChunkName: "vue-lazy" */'vue') + const { default: AssistantModal } = await import(/* webpackChunkName: "assistant-modal-lazy" */'./components/AssistantModal.vue') + Vue.mixin({ methods: { t, n } }) + + const modalId = 'assistantModal' + const modalElement = document.createElement('div') + modalElement.id = modalId + document.body.append(modalElement) + + const View = Vue.extend(AssistantModal) + const view = new View({ + propsData: { + // isInsideViewer, + input: task.input, + output: task.output, + selectedTaskTypeId: task.type, + readonly: true, + }, + }).$mount(modalElement) + + view.$on('cancel', () => { + view.$destroy() + }) +} + function init() { if (!OCA.TPAssistant) { /** * @namespace */ OCA.TPAssistant = { - openAssistant, + openTextProcessingModal, } } } init() +subscribeToNotifications()