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..92b3d3cd13e --- /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..ad1a26cb4bb --- /dev/null +++ b/src/apis/ai.ts @@ -0,0 +1,20 @@ +/** + * 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. + * + * @param fileId id of the file to tag. + */ +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..2ce7382dea4 100644 --- a/src/components/Menu/AssistantAction.vue +++ b/src/components/Menu/AssistantAction.vue @@ -175,6 +175,7 @@ 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' @@ -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..79f82afbf4f 100644 --- a/src/components/Modal/Translate.vue +++ b/src/components/Modal/Translate.vue @@ -97,6 +97,7 @@ 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 { markFileAsAiGenerated } from '../../apis/ai.ts' import { useIsMobileMixin } from '../Editor.provider.ts' export default { @@ -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..f918b0920e9 --- /dev/null +++ b/tests/unit/Controller/AiControllerTest.php @@ -0,0 +1,52 @@ +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); + } +}