From 70594f51e8c71635bcbe12365afd8466507fd4a3 Mon Sep 17 00:00:00 2001 From: samin-z Date: Fri, 10 Apr 2026 10:47:27 +0200 Subject: [PATCH 1/4] add AI tag Signed-off-by: samin-z --- appinfo/routes.php | 3 ++ composer/composer/autoload_classmap.php | 2 + composer/composer/autoload_static.php | 2 + lib/Controller/AiController.php | 32 +++++++++++++ lib/Service/AiTagService.php | 42 +++++++++++++++++ src/apis/ai.ts | 18 ++++++++ src/components/Editor.vue | 1 + src/components/Menu/AssistantAction.vue | 4 ++ src/components/Modal/Translate.vue | 11 +++++ tests/unit/Controller/AiControllerTest.php | 54 ++++++++++++++++++++++ 10 files changed, 169 insertions(+) create mode 100644 lib/Controller/AiController.php create mode 100644 lib/Service/AiTagService.php create mode 100644 src/apis/ai.ts create mode 100644 tests/unit/Controller/AiControllerTest.php diff --git a/appinfo/routes.php b/appinfo/routes.php index c0948b62873..0eb6c066aad 100644 --- a/appinfo/routes.php +++ b/appinfo/routes.php @@ -58,6 +58,9 @@ /** @see Controller\UserApiController::index() */ ['name' => 'UserApi#index', 'url' => '/api/v1/users', 'verb' => 'POST'], + + /** @see Controller\AiController::tagFile() */ + ['name' => 'Ai#tagFile', 'url' => '/ai/tag/{fileId}', 'verb' => 'POST'], ], 'ocs' => [ /** @see Controller\WorkspaceController::folder() */ diff --git a/composer/composer/autoload_classmap.php b/composer/composer/autoload_classmap.php index daa77de7338..a10fd62b7d4 100644 --- a/composer/composer/autoload_classmap.php +++ b/composer/composer/autoload_classmap.php @@ -9,6 +9,7 @@ 'Composer\\InstalledVersions' => $vendorDir . '/composer/InstalledVersions.php', 'OCA\\Text\\AppInfo\\Application' => $baseDir . '/../lib/AppInfo/Application.php', 'OCA\\Text\\Command\\ResetDocument' => $baseDir . '/../lib/Command/ResetDocument.php', + 'OCA\\Text\\Controller\\AiController' => $baseDir . '/../lib/Controller/AiController.php', 'OCA\\Text\\Controller\\AttachmentController' => $baseDir . '/../lib/Controller/AttachmentController.php', 'OCA\\Text\\Controller\\ISessionAwareController' => $baseDir . '/../lib/Controller/ISessionAwareController.php', 'OCA\\Text\\Controller\\NavigationController' => $baseDir . '/../lib/Controller/NavigationController.php', @@ -64,6 +65,7 @@ 'OCA\\Text\\Migration\\Version040100Date20240611165300' => $baseDir . '/../lib/Migration/Version040100Date20240611165300.php', 'OCA\\Text\\Migration\\Version070000Date20250925110024' => $baseDir . '/../lib/Migration/Version070000Date20250925110024.php', 'OCA\\Text\\Notification\\Notifier' => $baseDir . '/../lib/Notification/Notifier.php', + 'OCA\\Text\\Service\\AiTagService' => $baseDir . '/../lib/Service/AiTagService.php', 'OCA\\Text\\Service\\ApiService' => $baseDir . '/../lib/Service/ApiService.php', 'OCA\\Text\\Service\\AttachmentService' => $baseDir . '/../lib/Service/AttachmentService.php', 'OCA\\Text\\Service\\ConfigService' => $baseDir . '/../lib/Service/ConfigService.php', diff --git a/composer/composer/autoload_static.php b/composer/composer/autoload_static.php index ef2fa93317e..76362a01931 100644 --- a/composer/composer/autoload_static.php +++ b/composer/composer/autoload_static.php @@ -24,6 +24,7 @@ class ComposerStaticInitText 'Composer\\InstalledVersions' => __DIR__ . '/..' . '/composer/InstalledVersions.php', 'OCA\\Text\\AppInfo\\Application' => __DIR__ . '/..' . '/../lib/AppInfo/Application.php', 'OCA\\Text\\Command\\ResetDocument' => __DIR__ . '/..' . '/../lib/Command/ResetDocument.php', + 'OCA\\Text\\Controller\\AiController' => __DIR__ . '/..' . '/../lib/Controller/AiController.php', 'OCA\\Text\\Controller\\AttachmentController' => __DIR__ . '/..' . '/../lib/Controller/AttachmentController.php', 'OCA\\Text\\Controller\\ISessionAwareController' => __DIR__ . '/..' . '/../lib/Controller/ISessionAwareController.php', 'OCA\\Text\\Controller\\NavigationController' => __DIR__ . '/..' . '/../lib/Controller/NavigationController.php', @@ -79,6 +80,7 @@ class ComposerStaticInitText 'OCA\\Text\\Migration\\Version040100Date20240611165300' => __DIR__ . '/..' . '/../lib/Migration/Version040100Date20240611165300.php', 'OCA\\Text\\Migration\\Version070000Date20250925110024' => __DIR__ . '/..' . '/../lib/Migration/Version070000Date20250925110024.php', 'OCA\\Text\\Notification\\Notifier' => __DIR__ . '/..' . '/../lib/Notification/Notifier.php', + 'OCA\\Text\\Service\\AiTagService' => __DIR__ . '/..' . '/../lib/Service/AiTagService.php', 'OCA\\Text\\Service\\ApiService' => __DIR__ . '/..' . '/../lib/Service/ApiService.php', 'OCA\\Text\\Service\\AttachmentService' => __DIR__ . '/..' . '/../lib/Service/AttachmentService.php', 'OCA\\Text\\Service\\ConfigService' => __DIR__ . '/..' . '/../lib/Service/ConfigService.php', diff --git a/lib/Controller/AiController.php b/lib/Controller/AiController.php new file mode 100644 index 00000000000..7e9a44ff0f2 --- /dev/null +++ b/lib/Controller/AiController.php @@ -0,0 +1,32 @@ +aiTagService->tagFileAsAiGenerated($fileId); + return new DataResponse([]); + } +} diff --git a/lib/Service/AiTagService.php b/lib/Service/AiTagService.php new file mode 100644 index 00000000000..12c8ccc0663 --- /dev/null +++ b/lib/Service/AiTagService.php @@ -0,0 +1,42 @@ +systemTagObjectMapper->assignGeneratedByAITag((string)$fileId, 'files'); + } catch (\Exception $e) { + $this->logger->warning('Failed to tag file {fileId} as AI-generated: {error}', [ + 'fileId' => $fileId, + 'error' => $e->getMessage(), + 'exception' => $e, + ]); + } + } +} diff --git a/src/apis/ai.ts b/src/apis/ai.ts new file mode 100644 index 00000000000..23b9a6b1758 --- /dev/null +++ b/src/apis/ai.ts @@ -0,0 +1,18 @@ +/** + * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +import axios from '@nextcloud/axios' +import { generateUrl } from '@nextcloud/router' + +/** + * tag a file as containing AI-generated content. + */ +export async function markFileAsAiGenerated(fileId: number): Promise { + try { + await axios.post(generateUrl(`apps/text/ai/tag/${fileId}`)) + } catch (e) { + console.warn('failed to tag file as AI-generated', e) + } +} diff --git a/src/components/Editor.vue b/src/components/Editor.vue index 8b8442567b7..305a5b0bb46 100644 --- a/src/components/Editor.vue +++ b/src/components/Editor.vue @@ -73,6 +73,7 @@ diff --git a/src/components/Menu/AssistantAction.vue b/src/components/Menu/AssistantAction.vue index f5141a6cfb6..7bac2592024 100644 --- a/src/components/Menu/AssistantAction.vue +++ b/src/components/Menu/AssistantAction.vue @@ -179,6 +179,7 @@ import { useEditor } from '../../composables/useEditor.ts' import { useFileProps } from '../../composables/useFileProps.ts' import markdownit from '../../markdownit/index.js' import shouldInterpretAsMarkdown from '../../markdownit/shouldInterpretAsMarkdown.js' +import { markFileAsAiGenerated } from '../../apis/ai.ts' import { BaseActionEntry } from './BaseActionEntry.js' import { useMenuIDMixin } from './MenuBar.provider.js' @@ -373,6 +374,9 @@ export default { ? markdownit.render(task.output.output) : task.output.output this.editor.commands.insertContent(content) + if (this.fileId) { + await markFileAsAiGenerated(this.fileId) + } this.showTaskList = false }, async copyResult(task) { diff --git a/src/components/Modal/Translate.vue b/src/components/Modal/Translate.vue index ced91abd638..e51ea7a7fad 100644 --- a/src/components/Modal/Translate.vue +++ b/src/components/Modal/Translate.vue @@ -98,6 +98,7 @@ import NcModal from '@nextcloud/vue/components/NcModal' import NcSelect from '@nextcloud/vue/components/NcSelect' import NcTextArea from '@nextcloud/vue/components/NcTextArea' import { useIsMobileMixin } from '../Editor.provider.ts' +import { markFileAsAiGenerated } from '../../apis/ai.ts' export default { name: 'Translate', @@ -118,6 +119,10 @@ export default { type: String, default: '', }, + fileId: { + type: Number, + default: null, + }, }, data() { return { @@ -231,9 +236,15 @@ export default { } }, async contentInsert() { + if (this.fileId) { + await markFileAsAiGenerated(this.fileId) + } this.$emit('insert-content', this.result) }, async contentReplace() { + if (this.fileId) { + await markFileAsAiGenerated(this.fileId) + } this.$emit('replace-content', this.result) }, autosize() { diff --git a/tests/unit/Controller/AiControllerTest.php b/tests/unit/Controller/AiControllerTest.php new file mode 100644 index 00000000000..8c53e053a29 --- /dev/null +++ b/tests/unit/Controller/AiControllerTest.php @@ -0,0 +1,54 @@ +request = $this->createMock(IRequest::class); + $this->aiTagService = $this->createMock(AiTagService::class); + $this->controller = new AiController( + 'text', + $this->request, + $this->aiTagService, + ); + } + + public function testTagFileReturnsEmptyDataResponse(): void { + $this->aiTagService->expects($this->once()) + ->method('tagFileAsAiGenerated') + ->with(42); + + $response = $this->controller->tagFile(42); + + $this->assertInstanceOf(DataResponse::class, $response); + $this->assertSame([], $response->getData()); + } + + public function testTagFilePassesCorrectFileId(): void { + $this->aiTagService->expects($this->once()) + ->method('tagFileAsAiGenerated') + ->with(99999); + + $this->controller->tagFile(99999); + } +} From c30adb19057eae39431ee8098fa7f083e40f0557 Mon Sep 17 00:00:00 2001 From: samin-z Date: Fri, 10 Apr 2026 14:51:11 +0200 Subject: [PATCH 2/4] lint fixes Signed-off-by: samin-z --- lib/Service/AiTagService.php | 6 +++--- src/apis/ai.ts | 2 ++ src/components/Menu/AssistantAction.vue | 2 +- src/components/Modal/Translate.vue | 2 +- tests/unit/Controller/AiControllerTest.php | 2 +- 5 files changed, 8 insertions(+), 6 deletions(-) diff --git a/lib/Service/AiTagService.php b/lib/Service/AiTagService.php index 12c8ccc0663..92b3d3cd13e 100644 --- a/lib/Service/AiTagService.php +++ b/lib/Service/AiTagService.php @@ -21,11 +21,11 @@ public function __construct( /** * @param int $fileId - * + * * @return void - * + * * @throws \Exception - * + * * @since 34.0.0 (ISystemTagObjectMapper::assignGeneratedByAITag) */ public function tagFileAsAiGenerated(int $fileId): void { diff --git a/src/apis/ai.ts b/src/apis/ai.ts index 23b9a6b1758..ad1a26cb4bb 100644 --- a/src/apis/ai.ts +++ b/src/apis/ai.ts @@ -8,6 +8,8 @@ import { generateUrl } from '@nextcloud/router' /** * tag a file as containing AI-generated content. + * + * @param fileId id of the file to tag. */ export async function markFileAsAiGenerated(fileId: number): Promise { try { diff --git a/src/components/Menu/AssistantAction.vue b/src/components/Menu/AssistantAction.vue index 7bac2592024..2ce7382dea4 100644 --- a/src/components/Menu/AssistantAction.vue +++ b/src/components/Menu/AssistantAction.vue @@ -175,11 +175,11 @@ import TextBoxPlusOutlineIcon from 'vue-material-design-icons/TextBoxPlusOutline import TextShort from 'vue-material-design-icons/TextShort.vue' import TranslateVariant from 'vue-material-design-icons/Translate.vue' import DeleteOutlineIcon from 'vue-material-design-icons/TrashCanOutline.vue' +import { markFileAsAiGenerated } from '../../apis/ai.ts' import { useEditor } from '../../composables/useEditor.ts' import { useFileProps } from '../../composables/useFileProps.ts' import markdownit from '../../markdownit/index.js' import shouldInterpretAsMarkdown from '../../markdownit/shouldInterpretAsMarkdown.js' -import { markFileAsAiGenerated } from '../../apis/ai.ts' import { BaseActionEntry } from './BaseActionEntry.js' import { useMenuIDMixin } from './MenuBar.provider.js' diff --git a/src/components/Modal/Translate.vue b/src/components/Modal/Translate.vue index e51ea7a7fad..79f82afbf4f 100644 --- a/src/components/Modal/Translate.vue +++ b/src/components/Modal/Translate.vue @@ -97,8 +97,8 @@ import NcLoadingIcon from '@nextcloud/vue/components/NcLoadingIcon' import NcModal from '@nextcloud/vue/components/NcModal' import NcSelect from '@nextcloud/vue/components/NcSelect' import NcTextArea from '@nextcloud/vue/components/NcTextArea' -import { useIsMobileMixin } from '../Editor.provider.ts' import { markFileAsAiGenerated } from '../../apis/ai.ts' +import { useIsMobileMixin } from '../Editor.provider.ts' export default { name: 'Translate', diff --git a/tests/unit/Controller/AiControllerTest.php b/tests/unit/Controller/AiControllerTest.php index 8c53e053a29..4d01b5e3b51 100644 --- a/tests/unit/Controller/AiControllerTest.php +++ b/tests/unit/Controller/AiControllerTest.php @@ -16,7 +16,7 @@ use Test\TestCase; class AiControllerTest extends TestCase { - /** @var IRequest|MockObject*/ + /** @var IRequest|MockObject */ private $request; /** @var AiTagService|MockObject */ private $aiTagService; From cec7afb24ee039a710b8c50725afd75fe5377461 Mon Sep 17 00:00:00 2001 From: samin-z Date: Fri, 10 Apr 2026 16:20:59 +0200 Subject: [PATCH 3/4] apply comment suggestion Signed-off-by: samin-z --- tests/unit/Controller/AiControllerTest.php | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/tests/unit/Controller/AiControllerTest.php b/tests/unit/Controller/AiControllerTest.php index 4d01b5e3b51..f918b0920e9 100644 --- a/tests/unit/Controller/AiControllerTest.php +++ b/tests/unit/Controller/AiControllerTest.php @@ -16,10 +16,8 @@ use Test\TestCase; class AiControllerTest extends TestCase { - /** @var IRequest|MockObject */ - private $request; - /** @var AiTagService|MockObject */ - private $aiTagService; + private IRequest&MockObject $request; + private AiTagService&MockObject $aiTagService; private AiController $controller; protected function setUp(): void { From 073d35ac88475e790e71d78f125593411eee674c Mon Sep 17 00:00:00 2001 From: samin-z Date: Wed, 15 Apr 2026 11:05:42 +0200 Subject: [PATCH 4/4] remove unnecessary throws Signed-off-by: samin-z --- lib/Service/AiTagService.php | 2 -- 1 file changed, 2 deletions(-) diff --git a/lib/Service/AiTagService.php b/lib/Service/AiTagService.php index 92b3d3cd13e..3b257513d81 100644 --- a/lib/Service/AiTagService.php +++ b/lib/Service/AiTagService.php @@ -24,8 +24,6 @@ public function __construct( * * @return void * - * @throws \Exception - * * @since 34.0.0 (ISystemTagObjectMapper::assignGeneratedByAITag) */ public function tagFileAsAiGenerated(int $fileId): void {