diff --git a/appinfo/routes.php b/appinfo/routes.php index 7a6afb4880..b86087a2d4 100644 --- a/appinfo/routes.php +++ b/appinfo/routes.php @@ -42,6 +42,7 @@ // Direct Editing: Assets ['name' => 'assets#create', 'url' => 'assets', 'verb' => 'POST'], + ['name' => 'assets#createFromTask', 'url' => 'assets/tasks', 'verb' => 'POST'], ['name' => 'assets#get', 'url' => 'assets/{token}', 'verb' => 'GET'], // templates diff --git a/composer/composer/autoload_classmap.php b/composer/composer/autoload_classmap.php index 99384ca436..361a322655 100644 --- a/composer/composer/autoload_classmap.php +++ b/composer/composer/autoload_classmap.php @@ -87,6 +87,7 @@ 'OCA\\Richdocuments\\Settings\\Admin' => $baseDir . '/../lib/Settings/Admin.php', 'OCA\\Richdocuments\\Settings\\Personal' => $baseDir . '/../lib/Settings/Personal.php', 'OCA\\Richdocuments\\Settings\\Section' => $baseDir . '/../lib/Settings/Section.php', + 'OCA\\Richdocuments\\TaskProcessingManager' => $baseDir . '/../lib/TaskProcessingManager.php', 'OCA\\Richdocuments\\TemplateManager' => $baseDir . '/../lib/TemplateManager.php', 'OCA\\Richdocuments\\Template\\CollaboraTemplateProvider' => $baseDir . '/../lib/Template/CollaboraTemplateProvider.php', 'OCA\\Richdocuments\\TokenManager' => $baseDir . '/../lib/TokenManager.php', diff --git a/composer/composer/autoload_static.php b/composer/composer/autoload_static.php index 69c27e4de1..d773f0391e 100644 --- a/composer/composer/autoload_static.php +++ b/composer/composer/autoload_static.php @@ -120,6 +120,7 @@ class ComposerStaticInitRichdocuments 'OCA\\Richdocuments\\Settings\\Admin' => __DIR__ . '/..' . '/../lib/Settings/Admin.php', 'OCA\\Richdocuments\\Settings\\Personal' => __DIR__ . '/..' . '/../lib/Settings/Personal.php', 'OCA\\Richdocuments\\Settings\\Section' => __DIR__ . '/..' . '/../lib/Settings/Section.php', + 'OCA\\Richdocuments\\TaskProcessingManager' => __DIR__ . '/..' . '/../lib/TaskProcessingManager.php', 'OCA\\Richdocuments\\TemplateManager' => __DIR__ . '/..' . '/../lib/TemplateManager.php', 'OCA\\Richdocuments\\Template\\CollaboraTemplateProvider' => __DIR__ . '/..' . '/../lib/Template/CollaboraTemplateProvider.php', 'OCA\\Richdocuments\\TokenManager' => __DIR__ . '/..' . '/../lib/TokenManager.php', diff --git a/lib/Controller/AssetsController.php b/lib/Controller/AssetsController.php index b99f0d4b31..39c6d5cbc4 100644 --- a/lib/Controller/AssetsController.php +++ b/lib/Controller/AssetsController.php @@ -20,8 +20,10 @@ use OCP\Files\IRootFolder; use OCP\Files\NotFoundException; use OCP\Files\NotPermittedException; +use OCP\IL10N; use OCP\IRequest; use OCP\IURLGenerator; +use OCP\TaskProcessing\IManager; class AssetsController extends Controller { public function __construct( @@ -32,6 +34,8 @@ public function __construct( private ?string $userId, private UserScopeService $userScopeService, private IURLGenerator $urlGenerator, + private IManager $taskProcessingManager, + private IL10N $l10n, ) { parent::__construct($appName, $request); } @@ -77,15 +81,68 @@ public function create($path) { ]); } + /** + * @NoAdminRequired + * @NoCSRFRequired + * + * @param int $taskId + * @param array $fileIds + * @return JSONResponse + */ + public function createFromTask(int $taskId, array $fileIds): JSONResponse { + $task = $this->taskProcessingManager->getTask($taskId); + $taskOutput = $task->getOutput(); + $assets = []; + + if ($task->getUserId() !== $this->userId) { + return new JSONResponse([], Http::STATUS_FORBIDDEN); + } + + foreach ($fileIds as $fileId) { + $validFileIdForTask = array_key_exists($fileId, array_flip($taskOutput['images'])); + + if (!$validFileIdForTask) { + continue; + } + + $node = $this->rootFolder->getFirstNodeById($fileId); + if (is_null($node)) { + $node = $this->rootFolder->getFirstNodeByIdInPath($fileId, '/' . $this->rootFolder->getAppDataDirectoryName() . '/'); + } + + if (!($node instanceof File)) { + return new JSONResponse([], Http::STATUS_NOT_FOUND); + } + + $asset = $this->assetMapper->newAsset($this->userId, $node->getId()); + $assets[] = [ + 'filename' => $node->getName() . $node->getExtension(), + 'url' => $this->urlGenerator->linkToRouteAbsolute('richdocuments.assets.get', [ + 'token' => $asset->getToken(), + 'fromTask' => true, + ]), + ]; + } + + if (empty($assets)) { + return new JSONResponse([ + 'message' => $this->l10n->t('No files found for this task.'), + ], Http::STATUS_NOT_FOUND); + } + + return new JSONResponse($assets, Http::STATUS_CREATED); + } + /** * @PublicPage * @NoCSRFRequired * * @param string $token + * @param boolean $fromTask * @return Http\Response */ #[RestrictToWopiServer] - public function get($token) { + public function get($token, $fromTask = false) { try { $asset = $this->assetMapper->getAssetByToken($token); } catch (DoesNotExistException) { @@ -98,10 +155,17 @@ public function get($token) { $this->assetMapper->delete($asset); } - $this->userScopeService->setUserScope($asset->getUid()); - $userFolder = $this->rootFolder->getUserFolder($asset->getUid()); - $node = $userFolder->getFirstNodeById($asset->getFileid()); + + if ($fromTask) { + $node = $this->rootFolder->getFirstNodeById($asset->getFileid()); + if (is_null($node)) { + $node = $this->rootFolder->getFirstNodeByIdInPath($asset->getFileid(), '/' . $this->rootFolder->getAppDataDirectoryName() . '/'); + } + } else { + $userFolder = $this->rootFolder->getUserFolder($asset->getUid()); + $node = $userFolder->getFirstNodeById($asset->getFileid()); + } if ($node === null) { return new DataResponse([], Http::STATUS_NOT_FOUND); @@ -114,6 +178,7 @@ public function get($token) { $response = new StreamResponse($node->fopen('rb')); $response->addHeader('Content-Disposition', 'attachment'); $response->addHeader('Content-Type', 'application/octet-stream'); + return $response; } } diff --git a/lib/Controller/WopiController.php b/lib/Controller/WopiController.php index cef78e99d5..003e443c16 100644 --- a/lib/Controller/WopiController.php +++ b/lib/Controller/WopiController.php @@ -18,6 +18,7 @@ use OCA\Richdocuments\PermissionManager; use OCA\Richdocuments\Service\FederationService; use OCA\Richdocuments\Service\UserScopeService; +use OCA\Richdocuments\TaskProcessingManager; use OCA\Richdocuments\TemplateManager; use OCA\Richdocuments\TokenManager; use OCP\AppFramework\Controller; @@ -84,6 +85,7 @@ public function __construct( private IGroupManager $groupManager, private ILockManager $lockManager, private IEventDispatcher $eventDispatcher, + private TaskProcessingManager $taskProcessingManager, ) { parent::__construct($appName, $request); } @@ -120,6 +122,8 @@ public function checkFileInfo(string $fileId, string $access_token): JSONRespons $user = $this->userManager->get($wopi->getEditorUid()); $userDisplayName = $user !== null && !$isPublic ? $user->getDisplayName() : $wopi->getGuestDisplayname(); $isVersion = $version !== '0'; + $isSmartPickerEnabled = (bool)$wopi->getCanwrite() && !$isPublic && !$wopi->getDirect(); + $isTaskProcessingEnabled = $isSmartPickerEnabled && $this->taskProcessingManager->isTaskProcessingEnabled(); // If the file is locked manually by a user we want to open it read only for all others $canWriteThroughLock = true; @@ -157,7 +161,8 @@ public function checkFileInfo(string $fileId, string $access_token): JSONRespons 'DownloadAsPostMessage' => $wopi->getDirect(), 'SupportsLocks' => $this->lockManager->isLockProviderAvailable(), 'IsUserLocked' => $this->permissionManager->userIsFeatureLocked($wopi->getEditorUid()), - 'EnableRemoteLinkPicker' => (bool)$wopi->getCanwrite() && !$isPublic && !$wopi->getDirect(), + 'EnableRemoteLinkPicker' => $isSmartPickerEnabled, + 'EnableRemoteAIContent' => $isTaskProcessingEnabled, 'HasContentRange' => true, ]; diff --git a/lib/TaskProcessingManager.php b/lib/TaskProcessingManager.php new file mode 100644 index 0000000000..cb3da6564a --- /dev/null +++ b/lib/TaskProcessingManager.php @@ -0,0 +1,34 @@ +taskProcessing->getAvailableTaskTypes(), + array_flip(self::SUPPORTED_TASK_TYPES) + ); + + return !empty($availableTaskTypes); + } +} diff --git a/src/mixins/assistant.js b/src/mixins/assistant.js new file mode 100644 index 0000000000..ebd89502cd --- /dev/null +++ b/src/mixins/assistant.js @@ -0,0 +1,76 @@ +/** + * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +import { translate as t } from '@nextcloud/l10n' +import { generateUrl } from '@nextcloud/router' +import axios from '@nextcloud/axios' + +const SupportedTaskTypes = { + Text: 'core:text2text', + Image: 'core:text2image', +} + +export default { + data() { + return { + task: null, + } + }, + methods: { + async openAssistant() { + this.task = await window.OCA.Assistant.openAssistantForm({ + appId: 'richdocuments', + customId: 'richdocuments:' + this.fileid, + isInsideViewer: true, + actionButtons: [ + { + label: t('richdocuments', 'Insert into document'), + title: t('richdocuments', 'Insert into document'), + onClick: () => this.handleTask(this.task), + }, + ], + }) + }, + handleTask(task) { + switch (task.type) { + + case SupportedTaskTypes.Text: + this.insertAIText(task.output.output) + break + + case SupportedTaskTypes.Image: + this.insertAIImages(task.output.images) + break + + default: + break + } + }, + insertAIText(text) { + this.sendPostMessage('Action_Paste', { + Mimetype: 'text/plain;charset=utf-8', + Data: text, + }) + }, + async insertAIImages(images) { + const assets = await axios({ + method: 'post', + url: generateUrl('apps/richdocuments/assets/tasks'), + data: { + taskId: this.task.id, + fileIds: [images[0]], + }, + }) + + // For now, we only insert the first generated image + const firstImage = assets.data[0] + + this.sendPostMessage('Action_InsertGraphic', { + filename: firstImage.filename, + url: firstImage.url, + }) + }, + }, +} diff --git a/src/view/Office.vue b/src/view/Office.vue index 4e5194fe48..c1426d3209 100644 --- a/src/view/Office.vue +++ b/src/view/Office.vue @@ -114,6 +114,7 @@ import Config from '../services/config.tsx' import autoLogout from '../mixins/autoLogout.js' import openLocal from '../mixins/openLocal.js' import pickLink from '../mixins/pickLink.js' +import assistant from '../mixins/assistant.js' import saveAs from '../mixins/saveAs.js' import uiMention from '../mixins/uiMention.js' import version from '../mixins/version.js' @@ -140,7 +141,7 @@ export default { ZoteroHint, }, mixins: [ - autoLogout, openLocal, pickLink, saveAs, uiMention, version, + autoLogout, openLocal, pickLink, saveAs, uiMention, version, assistant, ], props: { filename: { @@ -467,6 +468,9 @@ export default { case 'UI_PickLink': this.pickLink() break + case 'UI_InsertAIContent': + this.openAssistant() + break case 'Action_GetLinkPreview': this.resolveLink(args.url) break