Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions lib/AppInfo/Application.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@
use OCA\Assistant\Listener\FileActionTaskSuccessfulListener;
use OCA\Assistant\Listener\FreePrompt\FreePromptReferenceListener;
use OCA\Assistant\Listener\LoadAdditionalScriptsListener;
use OCA\Assistant\Listener\NewFileMenuTaskFailedListener;
use OCA\Assistant\Listener\NewFileMenuTaskSuccessfulListener;
use OCA\Assistant\Listener\SpeechToText\SpeechToTextReferenceListener;
use OCA\Assistant\Listener\TaskFailedListener;
use OCA\Assistant\Listener\TaskOutputFileReferenceListener;
Expand Down Expand Up @@ -74,7 +76,9 @@ public function register(IRegistrationContext $context): void {
$context->registerEventListener(TaskFailedEvent::class, TaskFailedListener::class);
$context->registerEventListener(TaskSuccessfulEvent::class, ChattyLLMTaskListener::class);
$context->registerEventListener(TaskSuccessfulEvent::class, FileActionTaskSuccessfulListener::class);
$context->registerEventListener(TaskSuccessfulEvent::class, NewFileMenuTaskSuccessfulListener::class);
$context->registerEventListener(TaskFailedEvent::class, FileActionTaskFailedListener::class);
$context->registerEventListener(TaskFailedEvent::class, NewFileMenuTaskFailedListener::class);

$context->registerNotifierService(Notifier::class);

Expand Down
17 changes: 15 additions & 2 deletions lib/Listener/LoadAdditionalScriptsListener.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,10 @@
use OCP\AppFramework\Services\IInitialState;
use OCP\EventDispatcher\Event;
use OCP\EventDispatcher\IEventListener;
use OCP\IAppConfig;
use OCP\TaskProcessing\IManager as ITaskProcessingManager;
use OCP\TaskProcessing\TaskTypes\AudioToText;
use OCP\TaskProcessing\TaskTypes\TextToImage;
use OCP\TaskProcessing\TaskTypes\TextToTextSummary;
use OCP\Util;

Expand All @@ -23,6 +25,7 @@
class LoadAdditionalScriptsListener implements IEventListener {

public function __construct(
private IAppConfig $appConfig,
private IInitialState $initialStateService,
private ITaskProcessingManager $taskProcessingManager,
) {
Expand All @@ -34,15 +37,25 @@ public function handle(Event $event): void {
}

$availableTaskTypes = $this->taskProcessingManager->getAvailableTaskTypes();

// file actions
$summarizeAvailable = array_key_exists(TextToTextSummary::ID, $availableTaskTypes);
$sttAvailable = array_key_exists(AudioToText::ID, $availableTaskTypes);
$ttsAvailable = class_exists('OCP\\TaskProcessing\\TaskTypes\\TextToSpeech')
&& array_key_exists(\OCP\TaskProcessing\TaskTypes\TextToSpeech::ID, $availableTaskTypes);

$this->initialStateService->provideInitialState('stt-available', $sttAvailable);
$this->initialStateService->provideInitialState('tts-available', $ttsAvailable);
$this->initialStateService->provideInitialState('summarize-available', $summarizeAvailable);

Util::addInitScript(Application::APP_ID, Application::APP_ID . '-fileActions');

// New file menu to generate images
$isNewFileMenuEnabled = $this->appConfig->getValueInt(Application::APP_ID, 'new_image_file_menu_plugin', 1) === 1;
if ($isNewFileMenuEnabled) {
$hasText2Image = array_key_exists(TextToImage::ID, $availableTaskTypes);
$this->initialStateService->provideInitialState('new-file-generate-image', [
'hasText2Image' => $hasText2Image,
]);
Util::addScript(Application::APP_ID, Application::APP_ID . '-filesNewMenu');
}
}
}
53 changes: 53 additions & 0 deletions lib/Listener/NewFileMenuTaskFailedListener.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
<?php

/**
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

namespace OCA\Assistant\Listener;

use OCA\Assistant\Service\NotificationService;
use OCP\EventDispatcher\Event;
use OCP\EventDispatcher\IEventListener;
use OCP\Files\IRootFolder;
use OCP\TaskProcessing\Events\TaskFailedEvent;

/**
* @template-implements IEventListener<Event>
*/
class NewFileMenuTaskFailedListener implements IEventListener {

public function __construct(
private NotificationService $notificationService,
private IRootFolder $rootFolder,
) {
}

public function handle(Event $event): void {
if (!$event instanceof TaskFailedEvent) {
return;
}

$task = $event->getTask();
if ($task->getUserId() === null) {
return;
}

$customIdPattern = '/^new-image-file:(\d+)$/';
$isNewImageFileAction = preg_match($customIdPattern, $task->getCustomId(), $matches) === 1;

// For tasks with customId "new-image-file:<directoryIdNumber>" we always send a notification
if (!$isNewImageFileAction) {
return;
}

$directoryId = (int)$matches[1];
$userFolder = $this->rootFolder->getUserFolder($task->getUserId());
$directory = $userFolder->getFirstNodeById($directoryId);
$this->notificationService->sendNewImageFileNotification(
$task->getUserId(), $task->getId(),
$directoryId, $directory->getName(), $userFolder->getRelativePath($directory->getPath()),
);
}
}
68 changes: 68 additions & 0 deletions lib/Listener/NewFileMenuTaskSuccessfulListener.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
<?php

/**
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

namespace OCA\Assistant\Listener;

use OCA\Assistant\Service\AssistantService;
use OCA\Assistant\Service\NotificationService;
use OCP\EventDispatcher\Event;
use OCP\EventDispatcher\IEventListener;
use OCP\Files\IRootFolder;
use OCP\TaskProcessing\Events\TaskSuccessfulEvent;
use Psr\Log\LoggerInterface;

/**
* @template-implements IEventListener<Event>
*/
class NewFileMenuTaskSuccessfulListener implements IEventListener {

public function __construct(
private NotificationService $notificationService,
private AssistantService $assistantService,
private LoggerInterface $logger,
private IRootFolder $rootFolder,
) {
}

public function handle(Event $event): void {
if (!$event instanceof TaskSuccessfulEvent) {
return;
}

$task = $event->getTask();
if ($task->getUserId() === null) {
return;
}

$customIdPattern = '/^new-image-file:(\d+)$/';
$isNewImageFileAction = preg_match($customIdPattern, $task->getCustomId(), $matches) === 1;

// For tasks with customId "new-image-file:<directoryIdNumber>" we always send a notification
if (!$isNewImageFileAction) {
return;
}

$directoryId = (int)$matches[1];
$fileId = (int)$task->getOutput()['images'][0];
try {
$targetFile = $this->assistantService->saveNewFileMenuActionFile($task->getUserId(), $task->getId(), $fileId, $directoryId);
$userFolder = $this->rootFolder->getUserFolder($task->getUserId());
$directory = $targetFile->getParent();
$this->notificationService->sendNewImageFileNotification(
$task->getUserId(), $task->getId(),
$directoryId, $directory->getName(), $userFolder->getRelativePath($directory->getPath()),
$targetFile->getId(), $targetFile->getName(), $userFolder->getRelativePath($targetFile->getPath()),
);
} catch (\Exception $e) {
$this->logger->error('TaskSuccessfulListener: Failed to save new file menu action file.', [
'task' => $task->jsonSerialize(),
'exception' => $e,
]);
}

}
}
64 changes: 64 additions & 0 deletions lib/Notification/Notifier.php
Original file line number Diff line number Diff line change
Expand Up @@ -274,6 +274,70 @@ public function prepare(INotification $notification, string $languageCode): INot

return $notification;

case 'new_image_file_success':
$subject = $l->t('New image file has been generated');

$targetDirLink = $this->url->linkToRouteAbsolute('files.viewcontroller.showFile', ['fileid' => $params['target_directory_id']]);
$targetFileLink = $this->url->linkToRouteAbsolute('files.viewcontroller.showFile', ['fileid' => $params['target_file_id']]);
$taskLink = $params['target'];
$iconUrl = $this->url->getAbsoluteURL($this->url->imagePath(Application::APP_ID, 'app-dark.svg'));

$message = $l->t('{targetFile} has been generated in {targetDirectory}');

$notification
->setParsedSubject($subject)
->setRichMessage($message, [
'targetDirectory' => [
'type' => 'file',
'id' => (string)$params['target_directory_id'],
'name' => $params['target_directory_name'],
'path' => $params['target_directory_path'],
'link' => $targetDirLink,
],
'targetFile' => [
'type' => 'file',
'id' => (string)$params['target_file_id'],
'name' => $params['target_file_name'],
'path' => $params['target_file_path'],
'link' => $targetFileLink,
],
])
->setLink($taskLink)
->setIcon($iconUrl);

$actionLabel = $l->t('View results');
$action = $notification->createAction();
$action->setLabel($actionLabel)
->setParsedLabel($actionLabel)
->setLink($taskLink, IAction::TYPE_WEB)
->setPrimary(true);

$notification->addParsedAction($action);

return $notification;

case 'new_image_file_failure':
$subject = $l->t('Image file generation has failed');

$iconUrl = $this->url->getAbsoluteURL($this->url->imagePath(Application::APP_ID, 'app-dark.svg'));

$message = $l->t('Generation of a new image file in {targetDirectory} has failed');

$notification
->setParsedSubject($subject)
->setRichMessage($message, [
'targetDirectory' => [
'type' => 'file',
'id' => (string)$params['target_directory_id'],
'name' => $params['target_directory_name'],
'path' => $params['target_directory_path'],
'link' => $this->url->linkToRouteAbsolute('files.viewcontroller.showFile', ['fileid' => $params['target_directory_id']]),
],
])
->setIcon($iconUrl);

return $notification;

default:
// Unknown subject => Unknown notification => throw
throw new InvalidArgumentException();
Expand Down
19 changes: 19 additions & 0 deletions lib/Service/AssistantService.php
Original file line number Diff line number Diff line change
Expand Up @@ -533,6 +533,25 @@ private function saveFile(string $userId, int $ocpTaskId, int $fileId): File {
}
}

/**
* @throws TaskProcessingException
* @throws NotPermittedException
* @throws NotFoundException
* @throws Exception
* @throws LockedException
* @throws NoUserException
*/
public function saveNewFileMenuActionFile(string $userId, int $ocpTaskId, int $fileId, int $targetDirectoryId): File {
$taskOutputFile = $this->getTaskOutputFile($userId, $ocpTaskId, $fileId);
$userFolder = $this->rootFolder->getUserFolder($userId);
$targetDirectory = $userFolder->getFirstNodeById($targetDirectoryId);
if (!$targetDirectory instanceof Folder) {
throw new NotFoundException('Target directory not found: ' . $targetDirectoryId);
}
$targetFileName = $this->getTargetFileName($taskOutputFile);
return $targetDirectory->newFile($targetFileName, $taskOutputFile->fopen('rb'));
}

/**
* @param string $userId
* @param int $ocpTaskId
Expand Down
33 changes: 33 additions & 0 deletions lib/Service/NotificationService.php
Original file line number Diff line number Diff line change
Expand Up @@ -100,4 +100,37 @@ public function sendFileActionNotification(

$manager->notify($notification);
}

public function sendNewImageFileNotification(
string $userId, int $taskId,
?int $targetDirectoryId = null, ?string $targetDirectoryName = null, ?string $targetDirectoryPath = null,
?int $targetFileId = null, ?string $targetFileName = null, ?string $targetFilePath = null,
): void {
$manager = $this->notificationManager;
$notification = $manager->createNotification();

$params = [
'target_directory_id' => $targetDirectoryId,
'target_directory_name' => $targetDirectoryName,
'target_directory_path' => $targetDirectoryPath,
'target_file_id' => $targetFileId,
'target_file_name' => $targetFileName,
'target_file_path' => $targetFilePath,
'task_id' => $taskId,
'target' => $this->getDefaultTarget($taskId),
];
$taskSuccessful = $targetFileId !== null && $targetFileName !== null;

$subject = $taskSuccessful
? 'new_image_file_success'
: 'new_image_file_failure';

$notification->setApp(Application::APP_ID)
->setUser($userId)
->setDateTime(new DateTime())
->setObject('task', (string)$taskId)
->setSubject($subject, $params);

$manager->notify($notification);
}
}
8 changes: 4 additions & 4 deletions src/assistant.js
Original file line number Diff line number Diff line change
Expand Up @@ -521,15 +521,15 @@ export async function openAssistantTask(
let lastTask = task

modalMountPoint.addEventListener('cancel', () => {
view.unmount()
app.unmount()
})
modalMountPoint.addEventListener('submit', (data) => {
scheduleTask(task.appId, task.identifier ?? '', data.detail.selectedTaskTypeId, data.detail.inputs)
.then((response) => {
console.debug('scheduled task', response.data?.ocs?.data?.task)
})
.catch(error => {
view.unmount()
app.unmount()
console.error('Assistant scheduling error', error)
showError(
t('assistant', 'Assistant failed to schedule your task')
Expand Down Expand Up @@ -580,7 +580,7 @@ export async function openAssistantTask(
})
})
.catch(error => {
view.unmount()
app.unmount()
console.error('Assistant scheduling error', error)
showError(t('assistant', 'Assistant error') + ': ' + error?.response?.data)
// reject(new Error('Assistant scheduling error'))
Expand Down Expand Up @@ -694,7 +694,7 @@ export async function openAssistantTask(
lastTask.output = data.detail.output
data.detail.button.onClick(lastTask)
}
view.unmount()
app.unmount()
})
}

Expand Down
Loading
Loading