Skip to content

Commit

Permalink
feat: insert ai-generated text and images
Browse files Browse the repository at this point in the history
Signed-off-by: Elizabeth Danzberger <lizzy7128@tutanota.de>
  • Loading branch information
elzody committed Dec 17, 2024
1 parent cb86d80 commit bb6d9f0
Show file tree
Hide file tree
Showing 8 changed files with 193 additions and 6 deletions.
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
34 changes: 34 additions & 0 deletions lib/TaskProcessingManager.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<?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\TaskProcessing\IManager;

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

public function __construct(
private IManager $taskProcessing,
) {
}

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(
$this->taskProcessing->getAvailableTaskTypes(),
array_flip(self::SUPPORTED_TASK_TYPES)
);

return !empty($availableTaskTypes);
}
}
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

0 comments on commit bb6d9f0

Please sign in to comment.