Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: insert text and images generated with nextcloud assistant #4333

Merged
merged 2 commits into from
Dec 17, 2024
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
1 change: 1 addition & 0 deletions appinfo/routes.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions composer/composer/autoload_classmap.php
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
1 change: 1 addition & 0 deletions composer/composer/autoload_static.php
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
73 changes: 69 additions & 4 deletions lib/Controller/AssetsController.php
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -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);
}
Expand Down Expand Up @@ -77,15 +81,68 @@ public function create($path) {
]);
}

/**
* @NoAdminRequired
* @NoCSRFRequired
*
* @param int $taskId
* @param array<int> $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) {
Expand All @@ -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);
Expand All @@ -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;
}
}
7 changes: 6 additions & 1 deletion lib/Controller/WopiController.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -84,6 +85,7 @@ public function __construct(
private IGroupManager $groupManager,
private ILockManager $lockManager,
private IEventDispatcher $eventDispatcher,
private TaskProcessingManager $taskProcessingManager,
) {
parent::__construct($appName, $request);
}
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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,
];

Expand Down
41 changes: 41 additions & 0 deletions lib/TaskProcessingManager.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
<?php
/**
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

declare(strict_types=1);

namespace OCA\Richdocuments;

use OCP\App\IAppManager;
use OCP\IUserSession;
use OCP\TaskProcessing\IManager;

class TaskProcessingManager {
public const SUPPORTED_TASK_TYPES = [
'core:text2text',
'core:text2image',
];

public function __construct(
private IManager $taskProcessing,
private IAppManager $appManager,
private IUserSession $userSession,
) {
}

public function isTaskProcessingEnabled(): bool {
// Check if task processing should be considered enabled
// if any of our supported task types are available
$availableTaskTypes = array_intersect_key(
elzody marked this conversation as resolved.
Show resolved Hide resolved
$this->taskProcessing->getAvailableTaskTypes(),
array_flip(self::SUPPORTED_TASK_TYPES)
);

// Check if the Assistant is actually enabled for the user
$isAssistantEnabled = $this->appManager->isEnabledForUser('assistant', $this->userSession->getUser());

return !empty($availableTaskTypes) && $isAssistantEnabled;
}
}
76 changes: 76 additions & 0 deletions src/mixins/assistant.js
Original file line number Diff line number Diff line change
@@ -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,
})
},
},
}
6 changes: 5 additions & 1 deletion src/view/Office.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -140,7 +141,7 @@ export default {
ZoteroHint,
},
mixins: [
autoLogout, openLocal, pickLink, saveAs, uiMention, version,
autoLogout, openLocal, pickLink, saveAs, uiMention, version, assistant,
],
props: {
filename: {
Expand Down Expand Up @@ -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
Expand Down
Loading