diff options
Diffstat (limited to 'apps')
29 files changed, 603 insertions, 55 deletions
diff --git a/apps/settings/appinfo/info.xml b/apps/settings/appinfo/info.xml index 92d2928cd31..fefa2fadaef 100644 --- a/apps/settings/appinfo/info.xml +++ b/apps/settings/appinfo/info.xml @@ -19,6 +19,7 @@ <settings> <admin>OCA\Settings\Settings\Admin\Mail</admin> <admin>OCA\Settings\Settings\Admin\Overview</admin> + <admin>OCA\Settings\Settings\Admin\ArtificialIntelligence</admin> <admin>OCA\Settings\Settings\Admin\Server</admin> <admin>OCA\Settings\Settings\Admin\Sharing</admin> <admin>OCA\Settings\Settings\Admin\Security</admin> @@ -27,6 +28,7 @@ <admin-section>OCA\Settings\Sections\Admin\Delegation</admin-section> <admin-section>OCA\Settings\Sections\Admin\Groupware</admin-section> <admin-section>OCA\Settings\Sections\Admin\Overview</admin-section> + <admin-section>OCA\Settings\Sections\Admin\ArtificialIntelligence</admin-section> <admin-section>OCA\Settings\Sections\Admin\Security</admin-section> <admin-section>OCA\Settings\Sections\Admin\Server</admin-section> <admin-section>OCA\Settings\Sections\Admin\Sharing</admin-section> diff --git a/apps/settings/appinfo/routes.php b/apps/settings/appinfo/routes.php index 938842dd576..e238510b1a7 100644 --- a/apps/settings/appinfo/routes.php +++ b/apps/settings/appinfo/routes.php @@ -75,6 +75,7 @@ return [ ['name' => 'ChangePassword#changeUserPassword', 'url' => '/settings/users/changepassword', 'verb' => 'POST' , 'root' => ''], ['name' => 'TwoFactorSettings#index', 'url' => '/settings/api/admin/twofactorauth', 'verb' => 'GET' , 'root' => ''], ['name' => 'TwoFactorSettings#update', 'url' => '/settings/api/admin/twofactorauth', 'verb' => 'PUT' , 'root' => ''], + ['name' => 'AISettings#update', 'url' => '/settings/api/admin/ai', 'verb' => 'PUT' , 'root' => ''], ['name' => 'Help#help', 'url' => '/settings/help/{mode}', 'verb' => 'GET', 'defaults' => ['mode' => ''] , 'root' => ''], diff --git a/apps/settings/composer/composer/autoload_classmap.php b/apps/settings/composer/composer/autoload_classmap.php index 32de1ff6d2a..a2d62b53017 100644 --- a/apps/settings/composer/composer/autoload_classmap.php +++ b/apps/settings/composer/composer/autoload_classmap.php @@ -16,6 +16,7 @@ return array( 'OCA\\Settings\\Activity\\Setting' => $baseDir . '/../lib/Activity/Setting.php', 'OCA\\Settings\\AppInfo\\Application' => $baseDir . '/../lib/AppInfo/Application.php', 'OCA\\Settings\\BackgroundJobs\\VerifyUserData' => $baseDir . '/../lib/BackgroundJobs/VerifyUserData.php', + 'OCA\\Settings\\Controller\\AISettingsController' => $baseDir . '/../lib/Controller/AISettingsController.php', 'OCA\\Settings\\Controller\\AdminSettingsController' => $baseDir . '/../lib/Controller/AdminSettingsController.php', 'OCA\\Settings\\Controller\\AppSettingsController' => $baseDir . '/../lib/Controller/AppSettingsController.php', 'OCA\\Settings\\Controller\\AuthSettingsController' => $baseDir . '/../lib/Controller/AuthSettingsController.php', @@ -42,6 +43,7 @@ return array( 'OCA\\Settings\\Search\\AppSearch' => $baseDir . '/../lib/Search/AppSearch.php', 'OCA\\Settings\\Search\\SectionSearch' => $baseDir . '/../lib/Search/SectionSearch.php', 'OCA\\Settings\\Sections\\Admin\\Additional' => $baseDir . '/../lib/Sections/Admin/Additional.php', + 'OCA\\Settings\\Sections\\Admin\\ArtificialIntelligence' => $baseDir . '/../lib/Sections/Admin/ArtificialIntelligence.php', 'OCA\\Settings\\Sections\\Admin\\Delegation' => $baseDir . '/../lib/Sections/Admin/Delegation.php', 'OCA\\Settings\\Sections\\Admin\\Groupware' => $baseDir . '/../lib/Sections/Admin/Groupware.php', 'OCA\\Settings\\Sections\\Admin\\Overview' => $baseDir . '/../lib/Sections/Admin/Overview.php', @@ -56,6 +58,7 @@ return array( 'OCA\\Settings\\Service\\AuthorizedGroupService' => $baseDir . '/../lib/Service/AuthorizedGroupService.php', 'OCA\\Settings\\Service\\NotFoundException' => $baseDir . '/../lib/Service/NotFoundException.php', 'OCA\\Settings\\Service\\ServiceException' => $baseDir . '/../lib/Service/ServiceException.php', + 'OCA\\Settings\\Settings\\Admin\\ArtificialIntelligence' => $baseDir . '/../lib/Settings/Admin/ArtificialIntelligence.php', 'OCA\\Settings\\Settings\\Admin\\Delegation' => $baseDir . '/../lib/Settings/Admin/Delegation.php', 'OCA\\Settings\\Settings\\Admin\\Mail' => $baseDir . '/../lib/Settings/Admin/Mail.php', 'OCA\\Settings\\Settings\\Admin\\Overview' => $baseDir . '/../lib/Settings/Admin/Overview.php', diff --git a/apps/settings/composer/composer/autoload_static.php b/apps/settings/composer/composer/autoload_static.php index 57235766a7c..c76e3d84ae3 100644 --- a/apps/settings/composer/composer/autoload_static.php +++ b/apps/settings/composer/composer/autoload_static.php @@ -31,6 +31,7 @@ class ComposerStaticInitSettings 'OCA\\Settings\\Activity\\Setting' => __DIR__ . '/..' . '/../lib/Activity/Setting.php', 'OCA\\Settings\\AppInfo\\Application' => __DIR__ . '/..' . '/../lib/AppInfo/Application.php', 'OCA\\Settings\\BackgroundJobs\\VerifyUserData' => __DIR__ . '/..' . '/../lib/BackgroundJobs/VerifyUserData.php', + 'OCA\\Settings\\Controller\\AISettingsController' => __DIR__ . '/..' . '/../lib/Controller/AISettingsController.php', 'OCA\\Settings\\Controller\\AdminSettingsController' => __DIR__ . '/..' . '/../lib/Controller/AdminSettingsController.php', 'OCA\\Settings\\Controller\\AppSettingsController' => __DIR__ . '/..' . '/../lib/Controller/AppSettingsController.php', 'OCA\\Settings\\Controller\\AuthSettingsController' => __DIR__ . '/..' . '/../lib/Controller/AuthSettingsController.php', @@ -57,6 +58,7 @@ class ComposerStaticInitSettings 'OCA\\Settings\\Search\\AppSearch' => __DIR__ . '/..' . '/../lib/Search/AppSearch.php', 'OCA\\Settings\\Search\\SectionSearch' => __DIR__ . '/..' . '/../lib/Search/SectionSearch.php', 'OCA\\Settings\\Sections\\Admin\\Additional' => __DIR__ . '/..' . '/../lib/Sections/Admin/Additional.php', + 'OCA\\Settings\\Sections\\Admin\\ArtificialIntelligence' => __DIR__ . '/..' . '/../lib/Sections/Admin/ArtificialIntelligence.php', 'OCA\\Settings\\Sections\\Admin\\Delegation' => __DIR__ . '/..' . '/../lib/Sections/Admin/Delegation.php', 'OCA\\Settings\\Sections\\Admin\\Groupware' => __DIR__ . '/..' . '/../lib/Sections/Admin/Groupware.php', 'OCA\\Settings\\Sections\\Admin\\Overview' => __DIR__ . '/..' . '/../lib/Sections/Admin/Overview.php', @@ -71,6 +73,7 @@ class ComposerStaticInitSettings 'OCA\\Settings\\Service\\AuthorizedGroupService' => __DIR__ . '/..' . '/../lib/Service/AuthorizedGroupService.php', 'OCA\\Settings\\Service\\NotFoundException' => __DIR__ . '/..' . '/../lib/Service/NotFoundException.php', 'OCA\\Settings\\Service\\ServiceException' => __DIR__ . '/..' . '/../lib/Service/ServiceException.php', + 'OCA\\Settings\\Settings\\Admin\\ArtificialIntelligence' => __DIR__ . '/..' . '/../lib/Settings/Admin/ArtificialIntelligence.php', 'OCA\\Settings\\Settings\\Admin\\Delegation' => __DIR__ . '/..' . '/../lib/Settings/Admin/Delegation.php', 'OCA\\Settings\\Settings\\Admin\\Mail' => __DIR__ . '/..' . '/../lib/Settings/Admin/Mail.php', 'OCA\\Settings\\Settings\\Admin\\Overview' => __DIR__ . '/..' . '/../lib/Settings/Admin/Overview.php', diff --git a/apps/settings/img/ai.svg b/apps/settings/img/ai.svg new file mode 100644 index 00000000000..5d59fd6afe8 --- /dev/null +++ b/apps/settings/img/ai.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M12,2A2,2 0 0,1 14,4C14,4.74 13.6,5.39 13,5.73V7H14A7,7 0 0,1 21,14H22A1,1 0 0,1 23,15V18A1,1 0 0,1 22,19H21V20A2,2 0 0,1 19,22H5A2,2 0 0,1 3,20V19H2A1,1 0 0,1 1,18V15A1,1 0 0,1 2,14H3A7,7 0 0,1 10,7H11V5.73C10.4,5.39 10,4.74 10,4A2,2 0 0,1 12,2M7.5,13A2.5,2.5 0 0,0 5,15.5A2.5,2.5 0 0,0 7.5,18A2.5,2.5 0 0,0 10,15.5A2.5,2.5 0 0,0 7.5,13M16.5,13A2.5,2.5 0 0,0 14,15.5A2.5,2.5 0 0,0 16.5,18A2.5,2.5 0 0,0 19,15.5A2.5,2.5 0 0,0 16.5,13Z" /></svg>
\ No newline at end of file diff --git a/apps/settings/lib/Controller/AISettingsController.php b/apps/settings/lib/Controller/AISettingsController.php new file mode 100644 index 00000000000..7f016d79c25 --- /dev/null +++ b/apps/settings/lib/Controller/AISettingsController.php @@ -0,0 +1,70 @@ +<?php + +declare(strict_types=1); + +/** + * @copyright Copyright (c) 2023 Marcel Klehr <mklehr@gmx.net> + * + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License, version 3, + * along with this program. If not, see <http://www.gnu.org/licenses/> + * + */ +namespace OCA\Settings\Controller; + +use OCP\AppFramework\Controller; +use OCP\AppFramework\Http; +use OCP\AppFramework\Http\DataResponse; +use OCP\IConfig; +use OCP\IL10N; +use OCP\IRequest; +use OCP\IURLGenerator; +use OCP\IUserSession; +use OCP\Mail\IMailer; +use function GuzzleHttp\Promise\queue; + +class AISettingsController extends Controller { + + /** + * @param string $appName + * @param IRequest $request + * @param IConfig $config + */ + public function __construct( + $appName, + IRequest $request, + private IConfig $config, + ) { + parent::__construct($appName, $request); + } + + /** + * Sets the email settings + * + * @AuthorizedAdminSetting(settings=OCA\Settings\Settings\Admin\ArtificialIntelligence) + * + * @param array $settings + * @return DataResponse + */ + public function update($settings) { + $keys = ['ai.stt_provider', 'ai.textprocessing_provider_preferences', 'ai.translation_provider_preferences']; + foreach ($keys as $key) { + if (!isset($settings[$key])) { + continue; + } + $this->config->setAppValue('core', $key, json_encode($settings[$key])); + } + + return new DataResponse(); + } +} diff --git a/apps/settings/lib/Sections/Admin/ArtificialIntelligence.php b/apps/settings/lib/Sections/Admin/ArtificialIntelligence.php new file mode 100644 index 00000000000..1a25cdf5156 --- /dev/null +++ b/apps/settings/lib/Sections/Admin/ArtificialIntelligence.php @@ -0,0 +1,58 @@ +<?php + +declare(strict_types=1); + +/** + * @copyright Copyright (c) 2023 Marcel Klehr <mklehr@gmx.net> + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + */ +namespace OCA\Settings\Sections\Admin; + +use OCP\IL10N; +use OCP\IURLGenerator; +use OCP\Settings\IIconSection; + +class ArtificialIntelligence implements IIconSection { + + /** @var IL10N */ + private $l; + + /** @var IURLGenerator */ + private $urlGenerator; + + public function __construct(IL10N $l, IURLGenerator $urlGenerator) { + $this->l = $l; + $this->urlGenerator = $urlGenerator; + } + + public function getIcon(): string { + return $this->urlGenerator->imagePath('settings', 'ai.svg'); + } + + public function getID(): string { + return 'ai'; + } + + public function getName(): string { + return $this->l->t('Artificial Intelligence'); + } + + public function getPriority(): int { + return 40; + } +} diff --git a/apps/settings/lib/Settings/Admin/ArtificialIntelligence.php b/apps/settings/lib/Settings/Admin/ArtificialIntelligence.php new file mode 100644 index 00000000000..eb1983690a5 --- /dev/null +++ b/apps/settings/lib/Settings/Admin/ArtificialIntelligence.php @@ -0,0 +1,166 @@ +<?php + +declare(strict_types=1); + +/** + * @copyright Copyright (c) 2023 Marcel Klehr <mklehr@gmx.net> + * + * @author Marcel Klehr <mklehr@gmx.net> + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + */ +namespace OCA\Settings\Settings\Admin; + +use OCP\AppFramework\Http\TemplateResponse; +use OCP\AppFramework\Services\IInitialState; +use OCP\IConfig; +use OCP\IL10N; +use OCP\Settings\IDelegatedSettings; +use OCP\SpeechToText\ISpeechToTextManager; +use OCP\TextProcessing\IManager; +use OCP\TextProcessing\IProvider; +use OCP\TextProcessing\ITaskType; +use OCP\Translation\ITranslationManager; +use Psr\Container\ContainerExceptionInterface; +use Psr\Container\ContainerInterface; +use Psr\Container\NotFoundExceptionInterface; + +class ArtificialIntelligence implements IDelegatedSettings { + public function __construct( + private IConfig $config, + private IL10N $l, + private IInitialState $initialState, + private ITranslationManager $translationManager, + private ISpeechToTextManager $sttManager, + private IManager $textProcessingManager, + private ContainerInterface $container, + ) { + } + + /** + * @return TemplateResponse + */ + public function getForm() { + $translationProviders = []; + $translationPreferences = []; + foreach ($this->translationManager->getProviders() as $provider) { + $translationProviders[] = [ + 'class' => $provider::class, + 'name' => $provider->getName(), + ]; + $translationPreferences[] = $provider::class; + } + + $sttProviders = []; + foreach ($this->sttManager->getProviders() as $provider) { + $sttProviders[] = [ + 'class' => $provider::class, + 'name' => $provider->getName(), + ]; + } + + $textProcessingProviders = []; + /** @var array<class-string<ITaskType>, class-string<IProvider>> $textProcessingSettings */ + $textProcessingSettings = []; + foreach ($this->textProcessingManager->getProviders() as $provider) { + $textProcessingProviders[] = [ + 'class' => $provider::class, + 'name' => $provider->getName(), + 'taskType' => $provider->getTaskType(), + ]; + $textProcessingSettings[$provider->getTaskType()] = $provider::class; + } + $textProcessingTaskTypes = []; + foreach ($textProcessingSettings as $taskTypeClass => $providerClass) { + /** @var ITaskType $taskType */ + try { + $taskType = $this->container->get($taskTypeClass); + } catch (NotFoundExceptionInterface $e) { + continue; + } catch (ContainerExceptionInterface $e) { + continue; + } + $textProcessingTaskTypes[] = [ + 'class' => $taskTypeClass, + 'name' => $taskType->getName(), + 'description' => $taskType->getDescription(), + ]; + } + + $this->initialState->provideInitialState('ai-stt-providers', $sttProviders); + $this->initialState->provideInitialState('ai-translation-providers', $translationProviders); + $this->initialState->provideInitialState('ai-text-processing-providers', $textProcessingProviders); + $this->initialState->provideInitialState('ai-text-processing-task-types', $textProcessingTaskTypes); + + $settings = [ + 'ai.stt_provider' => count($sttProviders) > 0 ? $sttProviders[0]['class'] : null, + 'ai.textprocessing_provider_preferences' => $textProcessingSettings, + 'ai.translation_provider_preferences' => $translationPreferences, + ]; + foreach ($settings as $key => $defaultValue) { + $value = $defaultValue; + $json = $this->config->getAppValue('core', $key, ''); + if ($json !== '') { + $value = json_decode($json, true); + switch($key) { + case 'ai.textprocessing_provider_preferences': + // fill $value with $defaultValue values + $value = array_merge($defaultValue, $value); + break; + case 'ai.translation_provider_preferences': + $value += array_diff($defaultValue, $value); // Add entries from $defaultValue that are not in $value to the end of $value + break; + default: + break; + } + } + $settings[$key] = $value; + } + + $this->initialState->provideInitialState('ai-settings', $settings); + + return new TemplateResponse('settings', 'settings/admin/ai'); + } + + /** + * @return string the section ID, e.g. 'sharing' + */ + public function getSection() { + return 'ai'; + } + + /** + * @return int whether the form should be rather on the top or bottom of + * the admin section. The forms are arranged in ascending order of the + * priority values. It is required to return a value between 0 and 100. + * + * E.g.: 70 + */ + public function getPriority() { + return 10; + } + + public function getName(): ?string { + return $this->l->t('Artificial Intelligence'); + } + + public function getAuthorizedAppConfig(): array { + return [ + 'core' => ['/ai..*/'], + ]; + } +} diff --git a/apps/settings/src/admin.js b/apps/settings/src/admin.js index 3bbfb564763..c8d04049ded 100644 --- a/apps/settings/src/admin.js +++ b/apps/settings/src/admin.js @@ -242,7 +242,7 @@ window.addEventListener('DOMContentLoaded', () => { OC.SetupChecks.checkSetup(), OC.SetupChecks.checkGeneric(), OC.SetupChecks.checkWOFF2Loading(OC.filePath('core', '', 'fonts/NotoSans-Regular-latin.woff2'), OC.theme.docPlaceholderUrl), - OC.SetupChecks.checkDataProtected() + OC.SetupChecks.checkDataProtected(), ).then((check1, check2, check3, check4, check5, check6, check7, check8, check9, check10, check11) => { const messages = [].concat(check1, check2, check3, check4, check5, check6, check7, check8, check9, check10, check11) const $el = $('#postsetupchecks') diff --git a/apps/settings/src/components/AdminAI.vue b/apps/settings/src/components/AdminAI.vue new file mode 100644 index 00000000000..174c9000a9e --- /dev/null +++ b/apps/settings/src/components/AdminAI.vue @@ -0,0 +1,173 @@ +<template> + <div> + <NcSettingsSection :title="t('settings', 'Machine translation')" + :description="t('settings', 'Machine translation can be implemented by different apps. Here you can define the precedence of the machine translation apps you have installed at the moment.')"> + <draggable v-model="settings['ai.translation_provider_preferences']" @change="saveChanges"> + <div v-for="(providerClass, i) in settings['ai.translation_provider_preferences']" :key="providerClass" class="draggable__item"> + <DragVerticalIcon /> <span class="draggable__number">{{ i + 1 }}</span> {{ translationProviders.find(p => p.class === providerClass)?.name }} + <NcButton aria-label="Move up" type="tertiary" @click="moveUp(i)"> + <template #icon> + <ArrowUpIcon /> + </template> + </NcButton> + <NcButton aria-label="Move down" type="tertiary" @click="moveDown(i)"> + <template #icon> + <ArrowDownIcon /> + </template> + </NcButton> + </div> + </draggable> + </NcSettingsSection> + <NcSettingsSection :title="t('settings', 'Speech-To-Text')" + :description="t('settings', 'Speech-To-Text can be implemented by different apps. Here you can set which app should be used.')"> + <template v-for="provider in sttProviders"> + <NcCheckboxRadioSwitch :key="provider.class" + :checked.sync="settings['ai.stt_provider']" + :value="provider.class" + name="stt_provider" + type="radio" + @update:checked="saveChanges"> + {{ provider.name }} + </NcCheckboxRadioSwitch> + </template> + <template v-if="!hasStt"> + <NcCheckboxRadioSwitch disabled type="radio"> + {{ t('settings', 'None of your currently installed apps provide Speech-To-Text functionality') }} + </NcCheckboxRadioSwitch> + </template> + </NcSettingsSection> + <NcSettingsSection :title="t('settings', 'Text processing')" + :description="t('settings', 'Text processing tasks can be implemented by different apps. Here you can set which app should be used for which task.')"> + <template v-for="type in Object.keys(settings['ai.textprocessing_provider_preferences'])"> + <div :key="type"> + <h3>{{ t('settings', 'Task:') }} {{ getTaskType(type).name }}</h3> + <p>{{ getTaskType(type).description }}</p> + <p> </p> + <NcSelect v-model="settings['ai.textprocessing_provider_preferences'][type]" + :clearable="false" + :options="textProcessingProviders.filter(p => p.taskType === type).map(p => p.class)" + @input="saveChanges"> + <template #option="{label}"> + {{ textProcessingProviders.find(p => p.class === label)?.name }} + </template> + <template #selected-option="{label}"> + {{ textProcessingProviders.find(p => p.class === label)?.name }} + </template> + </NcSelect> + <p> </p> + </div> + </template> + <template v-if="!hasTextProcessing"> + <p>{{ t('settings', 'None of your currently installed apps provide Text processing functionality') }}</p> + </template> + </NcSettingsSection> + </div> +</template> + +<script> +import axios from '@nextcloud/axios' +import NcCheckboxRadioSwitch from '@nextcloud/vue/dist/Components/NcCheckboxRadioSwitch.js' +import NcSettingsSection from '@nextcloud/vue/dist/Components/NcSettingsSection.js' +import NcSelect from '@nextcloud/vue/dist/Components/NcSelect.js' +import NcButton from '@nextcloud/vue/dist/Components/NcButton.js' +import draggable from 'vuedraggable' +import DragVerticalIcon from 'vue-material-design-icons/DragVertical.vue' +import ArrowDownIcon from 'vue-material-design-icons/ArrowDown.vue' +import ArrowUpIcon from 'vue-material-design-icons/ArrowUp.vue' +import { loadState } from '@nextcloud/initial-state' + +import { generateUrl } from '@nextcloud/router' + +export default { + name: 'AdminAI', + components: { + NcCheckboxRadioSwitch, + NcSettingsSection, + NcSelect, + draggable, + DragVerticalIcon, + ArrowDownIcon, + ArrowUpIcon, + NcButton + }, + data() { + return { + loading: false, + dirty: false, + groups: [], + loadingGroups: false, + sttProviders: loadState('settings', 'ai-stt-providers'), + translationProviders: loadState('settings', 'ai-translation-providers'), + textProcessingProviders: loadState('settings', 'ai-text-processing-providers'), + textProcessingTaskTypes: loadState('settings', 'ai-text-processing-task-types'), + settings: loadState('settings', 'ai-settings'), + } + }, + computed: { + hasStt() { + return this.sttProviders.length > 0 + }, + hasTextProcessing() { + return Object.keys(this.settings['ai.textprocessing_provider_preferences']).length > 0 && Array.isArray(this.textProcessingTaskTypes) + }, + }, + methods: { + moveUp(i) { + this.settings['ai.translation_provider_preferences'].splice( + Math.min(i - 1, 0), + 0, + ...this.settings['ai.translation_provider_preferences'].splice(i, 1) + ) + this.saveChanges() + }, + moveDown(i) { + this.settings['ai.translation_provider_preferences'].splice( + i + 1, + 0, + ...this.settings['ai.translation_provider_preferences'].splice(i, 1) + ) + this.saveChanges() + }, + async saveChanges() { + this.loading = true + const data = { settings: this.settings } + try { + await axios.put(generateUrl('/settings/api/admin/ai'), data) + } catch (err) { + console.error('could not save changes', err) + } + this.loading = false + }, + getTaskType(type) { + if (!Array.isArray(this.textProcessingTaskTypes)) { + return null + } + return this.textProcessingTaskTypes.find(taskType => taskType.class === type) + }, + }, +} +</script> +<style scoped> +.draggable__item { + margin-bottom: 5px; + display: flex; + align-items: center; +} + +.draggable__item, +.draggable__item * { + cursor: grab; +} + +.draggable__number { + border-radius: 20px; + border: 2px solid var(--color-primary-default); + color: var(--color-primary-default); + padding: 0px 7px; + margin-right: 3px; +} + +.drag-vertical-icon { + float: left; +} +</style> diff --git a/apps/settings/src/components/AdminDelegation/GroupSelect.vue b/apps/settings/src/components/AdminDelegation/GroupSelect.vue index 82b5e51fb45..91593516760 100644 --- a/apps/settings/src/components/AdminDelegation/GroupSelect.vue +++ b/apps/settings/src/components/AdminDelegation/GroupSelect.vue @@ -1,6 +1,6 @@ <template> - <NcSelect :input-id="setting.id" - v-model="selected" + <NcSelect v-model="selected" + :input-id="setting.id" class="group-select" :placeholder="t('settings', 'None')" label="displayName" diff --git a/apps/settings/src/components/AdminTwoFactor.vue b/apps/settings/src/components/AdminTwoFactor.vue index 27e1b2f4e84..d45e7f7f6ff 100644 --- a/apps/settings/src/components/AdminTwoFactor.vue +++ b/apps/settings/src/components/AdminTwoFactor.vue @@ -22,8 +22,8 @@ <label for="enforcedGroups"> <span>{{ t('settings', 'Enforced groups') }}</span> </label> - <NcSelect input-id="enforcedGroups" - v-model="enforcedGroups" + <NcSelect v-model="enforcedGroups" + input-id="enforcedGroups" :options="groups" :disabled="loading" :multiple="true" @@ -38,8 +38,8 @@ <label for="excludedGroups"> <span>{{ t('settings', 'Excluded groups') }}</span> </label> - <NcSelect input-id="excludedGroups" - v-model="excludedGroups" + <NcSelect v-model="excludedGroups" + input-id="excludedGroups" :options="groups" :disabled="loading" :multiple="true" diff --git a/apps/settings/src/components/AuthTokenSection.vue b/apps/settings/src/components/AuthTokenSection.vue index 01a85f4ae1b..bb9bd3fb065 100644 --- a/apps/settings/src/components/AuthTokenSection.vue +++ b/apps/settings/src/components/AuthTokenSection.vue @@ -49,7 +49,7 @@ const confirm = () => { t('settings', 'Do you really want to wipe your data from this device?'), t('settings', 'Confirm wipe'), resolve, - true + true, ) }) } diff --git a/apps/settings/src/components/Markdown.vue b/apps/settings/src/components/Markdown.vue index fbbbf7456a1..dcbd44b186b 100644 --- a/apps/settings/src/components/Markdown.vue +++ b/apps/settings/src/components/Markdown.vue @@ -100,7 +100,7 @@ export default { 'del', 'blockquote', ], - } + }, ) }, }, diff --git a/apps/settings/src/components/PersonalInfo/AvatarSection.vue b/apps/settings/src/components/PersonalInfo/AvatarSection.vue index a69cf368d9f..d6fa8d52367 100644 --- a/apps/settings/src/components/PersonalInfo/AvatarSection.vue +++ b/apps/settings/src/components/PersonalInfo/AvatarSection.vue @@ -22,7 +22,9 @@ <template> <section id="vue-avatar-section"> - <h3 class="hidden-visually"> {{ t('settings', 'Your profile information') }} </h3> + <h3 class="hidden-visually"> + {{ t('settings', 'Your profile information') }} + </h3> <HeaderBar :input-id="avatarChangeSupported ? inputId : null" :readable="avatar.readable" :scope.sync="avatar.scope" /> @@ -30,13 +32,13 @@ <div v-if="!showCropper" class="avatar__container"> <div class="avatar__preview"> <NcAvatar v-if="!loading" + :key="version" :user="userId" :aria-label="t('settings', 'Your profile picture')" :disabled-menu="true" :disabled-tooltip="true" :show-user-status="false" - :size="180" - :key="version" /> + :size="180" /> <div v-else class="icon-loading" /> </div> <template v-if="avatarChangeSupported"> @@ -62,8 +64,8 @@ </NcButton> </div> <span>{{ t('settings', 'The file must be a PNG or JPG') }}</span> - <input ref="input" - :id="inputId" + <input :id="inputId" + ref="input" type="file" :accept="validMimeTypes.join(',')" @change="onChange"> diff --git a/apps/settings/src/components/PersonalInfo/DetailsSection.vue b/apps/settings/src/components/PersonalInfo/DetailsSection.vue index 10f9b757220..075ed6f71e2 100644 --- a/apps/settings/src/components/PersonalInfo/DetailsSection.vue +++ b/apps/settings/src/components/PersonalInfo/DetailsSection.vue @@ -29,7 +29,9 @@ <Account :size="20" /> <div class="details__groups-info"> <p>{{ t('settings', 'You are a member of the following groups:') }}</p> - <p class="details__groups-list">{{ groups.join(', ') }}</p> + <p class="details__groups-list"> + {{ groups.join(', ') }} + </p> </div> </div> <div class="details__quota"> @@ -69,6 +71,13 @@ export default { NcProgressBar, }, + data() { + return { + groups, + usageRelative, + } + }, + computed: { quotaText() { if (quota === SPACE_UNLIMITED) { @@ -79,14 +88,7 @@ export default { 'You are using <strong>{usage}</strong> of <strong>{totalSpace}</strong> (<strong>{usageRelative}%</strong>)', { usage, totalSpace, usageRelative }, ) - } - }, - - data() { - return { - groups, - usageRelative, - } + }, }, } </script> diff --git a/apps/settings/src/components/PersonalInfo/DisplayNameSection.vue b/apps/settings/src/components/PersonalInfo/DisplayNameSection.vue index b88f52e0df4..00a629c4d54 100644 --- a/apps/settings/src/components/PersonalInfo/DisplayNameSection.vue +++ b/apps/settings/src/components/PersonalInfo/DisplayNameSection.vue @@ -66,6 +66,6 @@ export default { } emit('settings:display-name:updated', value) }, - } + }, } </script> diff --git a/apps/settings/src/components/PersonalInfo/EmailSection/EmailSection.vue b/apps/settings/src/components/PersonalInfo/EmailSection/EmailSection.vue index 613b90356c3..980af3e62b0 100644 --- a/apps/settings/src/components/PersonalInfo/EmailSection/EmailSection.vue +++ b/apps/settings/src/components/PersonalInfo/EmailSection/EmailSection.vue @@ -153,7 +153,7 @@ export default { this.handleResponse( 'error', t('settings', 'Unable to update primary email address'), - e + e, ) } }, @@ -166,7 +166,7 @@ export default { this.handleResponse( 'error', t('settings', 'Unable to delete additional email address'), - e + e, ) } }, @@ -178,7 +178,7 @@ export default { this.handleResponse( 'error', t('settings', 'Unable to delete additional email address'), - {} + {}, ) } }, diff --git a/apps/settings/src/components/PersonalInfo/LanguageSection/Language.vue b/apps/settings/src/components/PersonalInfo/LanguageSection/Language.vue index 959e7153e17..bacc687c58e 100644 --- a/apps/settings/src/components/PersonalInfo/LanguageSection/Language.vue +++ b/apps/settings/src/components/PersonalInfo/LanguageSection/Language.vue @@ -88,7 +88,7 @@ export default { allLanguages() { return Object.freeze( [...this.commonLanguages, ...this.otherLanguages] - .reduce((acc, { code, name }) => ({ ...acc, [code]: name }), {}) + .reduce((acc, { code, name }) => ({ ...acc, [code]: name }), {}), ) }, }, diff --git a/apps/settings/src/components/PersonalInfo/LocaleSection/Locale.vue b/apps/settings/src/components/PersonalInfo/LocaleSection/Locale.vue index 7bb78f74cc2..6f296a6c124 100644 --- a/apps/settings/src/components/PersonalInfo/LocaleSection/Locale.vue +++ b/apps/settings/src/components/PersonalInfo/LocaleSection/Locale.vue @@ -50,7 +50,7 @@ <span>{{ example.time }}</span> </p> <p> - {{ t('settings', 'Week starts on {firstDayOfWeek}', { firstDayOfWeek: this.example.firstDayOfWeek }) }} + {{ t('settings', 'Week starts on {firstDayOfWeek}', { firstDayOfWeek: example.firstDayOfWeek }) }} </p> </div> </div> @@ -107,7 +107,7 @@ export default { allLocales() { return Object.freeze( [...this.localesForLanguage, ...this.otherLocales] - .reduce((acc, { code, name }) => ({ ...acc, [code]: name }), {}) + .reduce((acc, { code, name }) => ({ ...acc, [code]: name }), {}), ) }, }, diff --git a/apps/settings/src/components/PersonalInfo/shared/AccountPropertySection.vue b/apps/settings/src/components/PersonalInfo/shared/AccountPropertySection.vue index 2329cf123f5..59cccc54714 100644 --- a/apps/settings/src/components/PersonalInfo/shared/AccountPropertySection.vue +++ b/apps/settings/src/components/PersonalInfo/shared/AccountPropertySection.vue @@ -38,8 +38,8 @@ autocorrect="off" @input="onPropertyChange" /> <input v-else - ref="input" :id="inputId" + ref="input" :placeholder="placeholder" :type="type" :value="value" diff --git a/apps/settings/src/components/PersonalInfo/shared/HeaderBar.vue b/apps/settings/src/components/PersonalInfo/shared/HeaderBar.vue index 8db2a43f6bc..b149d8405f4 100644 --- a/apps/settings/src/components/PersonalInfo/shared/HeaderBar.vue +++ b/apps/settings/src/components/PersonalInfo/shared/HeaderBar.vue @@ -100,7 +100,7 @@ export default { isHeading: { type: Boolean, default: false, - } + }, }, data() { diff --git a/apps/settings/src/components/UserList.vue b/apps/settings/src/components/UserList.vue index d79d695f771..9fcf4d4e526 100644 --- a/apps/settings/src/components/UserList.vue +++ b/apps/settings/src/components/UserList.vue @@ -42,9 +42,9 @@ </NcEmptyContent> <RecycleScroller v-else + ref="scroller" class="user-list" :style="style" - ref="scroller" :items="filteredUsers" key-field="id" role="table" @@ -55,7 +55,6 @@ :item-size="rowHeight" @hook:mounted="handleMounted" @scroll-end="handleScrollEnd"> - <template #before> <caption class="hidden-visually"> {{ t('settings', 'List of users. This list is not fully rendered for performance reasons. The users will be rendered as you navigate through the list.') }} @@ -79,7 +78,6 @@ <UserListFooter :loading="loading.users" :filtered-users="filteredUsers" /> </template> - </RecycleScroller> </Fragment> </template> diff --git a/apps/settings/src/components/Users/NewUserModal.vue b/apps/settings/src/components/Users/NewUserModal.vue index d8a9eb64a3e..00ce7c92600 100644 --- a/apps/settings/src/components/Users/NewUserModal.vue +++ b/apps/settings/src/components/Users/NewUserModal.vue @@ -29,8 +29,8 @@ :disabled="loading.all" @submit.prevent="createUser"> <h2>{{ t('settings', 'New user') }}</h2> - <NcTextField class="modal__item" - ref="username" + <NcTextField ref="username" + class="modal__item" data-test="username" :value.sync="newUser.id" :disabled="settings.newUserGenerateUserID" @@ -50,12 +50,12 @@ autocomplete="off" autocorrect="off" /> <span v-if="!settings.newUserRequireEmail" - class="modal__hint" - id="password-email-hint"> + id="password-email-hint" + class="modal__hint"> {{ t('settings', 'Either password or email is required') }} </span> - <NcPasswordField class="modal__item" - ref="password" + <NcPasswordField ref="password" + class="modal__item" data-test="password" :value.sync="newUser.password" :minlength="minPasswordLength" @@ -81,8 +81,8 @@ <div class="modal__item"> <!-- hidden input trick for vanilla html5 form validation --> <NcTextField v-if="!settings.isAdmin" - tabindex="-1" id="new-user-groups-input" + tabindex="-1" :class="{ 'icon-loading-small': loading.groups }" :value="newUser.groups" :required="!settings.isAdmin" /> @@ -112,11 +112,11 @@ for="new-user-sub-admin"> {{ t('settings', 'Administered groups') }} </label> - <NcSelect class="modal__select" + <NcSelect v-model="newUser.subAdminsGroups" + class="modal__select" input-id="new-user-sub-admin" :placeholder="t('settings', 'Set user as admin for …')" :options="subAdminsGroups" - v-model="newUser.subAdminsGroups" :close-on-select="false" :multiple="true" label="name" /> @@ -126,11 +126,11 @@ for="new-user-quota"> {{ t('settings', 'Quota') }} </label> - <NcSelect class="modal__select" + <NcSelect v-model="newUser.quota" + class="modal__select" input-id="new-user-quota" :placeholder="t('settings', 'Set user quota')" :options="quotaOptions" - v-model="newUser.quota" :clearable="false" :taggable="true" :create-option="validateQuota" /> @@ -141,14 +141,14 @@ for="new-user-language"> {{ t('settings', 'Language') }} </label> - <NcSelect class="modal__select" + <NcSelect v-model="newUser.language" + class="modal__select" input-id="new-user-language" :placeholder="t('settings', 'Set default language')" :clearable="false" :selectable="option => !option.languages" :filter-by="languageFilterBy" :options="languages" - v-model="newUser.language" label="name" /> </div> <div :class="['modal__item managers', { 'icon-loading-small': loading.manager }]"> @@ -157,11 +157,11 @@ <!-- TRANSLATORS This string describes a manager in the context of an organization --> {{ t('settings', 'Manager') }} </label> - <NcSelect class="modal__select" + <NcSelect v-model="newUser.manager" + class="modal__select" input-id="new-user-manager" :placeholder="managerLabel" :options="possibleManagers" - v-model="newUser.manager" :user-select="true" label="displayname" @search="searchUserManager" /> @@ -366,7 +366,7 @@ export default { // Show group header of the language if (option.languages) { return option.languages.some( - ({ name }) => name.toLocaleLowerCase().includes(search.toLocaleLowerCase()) + ({ name }) => name.toLocaleLowerCase().includes(search.toLocaleLowerCase()), ) } diff --git a/apps/settings/src/components/Users/UserRow.vue b/apps/settings/src/components/Users/UserRow.vue index fcd9829db1e..b230e9a9bf8 100644 --- a/apps/settings/src/components/Users/UserRow.vue +++ b/apps/settings/src/components/Users/UserRow.vue @@ -44,8 +44,8 @@ {{ t('settings', 'Edit display name') }} </label> <NcTextField :id="'displayName' + uniqueId" - data-test="displayNameField" ref="displayNameField" + data-test="displayNameField" :show-trailing-button="true" class="user-row-text-field" :class="{ 'icon-loading-small': idState.loading.displayName }" @@ -202,8 +202,8 @@ </template> <template v-else-if="!isObfuscated"> <label :for="'quota-progress' + uniqueId">{{ userQuota }} ({{ usedSpace }})</label> - <NcProgressBar class="row__progress" - :id="'quota-progress' + uniqueId" + <NcProgressBar :id="'quota-progress' + uniqueId" + class="row__progress" :class="{ 'row__progress--warn': usedQuota > 80, }" diff --git a/apps/settings/src/main-admin-ai.js b/apps/settings/src/main-admin-ai.js new file mode 100644 index 00000000000..485b219ed78 --- /dev/null +++ b/apps/settings/src/main-admin-ai.js @@ -0,0 +1,39 @@ +/** + * @copyright Copyright (c) 2016 Christoph Wurst <christoph@winzerhof-wurst.at> + * + * @author Christoph Wurst <christoph@winzerhof-wurst.at> + * @author John Molakvoæ <skjnldsv@protonmail.com> + * @author Roeland Jago Douma <roeland@famdouma.nl> + * + * @license AGPL-3.0-or-later + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + */ + +import Vue from 'vue' + +import ArtificialIntelligence from './components/AdminAI.vue' + +// eslint-disable-next-line camelcase +__webpack_nonce__ = btoa(OC.requestToken) + +Vue.prototype.t = t + +// Not used here but required for legacy templates +window.OC = window.OC || {} +window.OC.Settings = window.OC.Settings || {} + +const View = Vue.extend(ArtificialIntelligence) +new View().$mount('#ai-settings') diff --git a/apps/settings/src/main-admin-security.js b/apps/settings/src/main-admin-security.js index f3279f45c93..a5c239683e7 100644 --- a/apps/settings/src/main-admin-security.js +++ b/apps/settings/src/main-admin-security.js @@ -39,7 +39,7 @@ window.OC = window.OC || {} window.OC.Settings = window.OC.Settings || {} store.replaceState( - loadState('settings', 'mandatory2FAState') + loadState('settings', 'mandatory2FAState'), ) const View = Vue.extend(AdminTwoFactor) diff --git a/apps/settings/src/utils/userUtils.ts b/apps/settings/src/utils/userUtils.ts index eff8315b693..b6c96624139 100644 --- a/apps/settings/src/utils/userUtils.ts +++ b/apps/settings/src/utils/userUtils.ts @@ -33,6 +33,8 @@ export const defaultQuota = { /** * Return `true` if the logged in user does not have permissions to view the * data of `user` + * @param user + * @param user.id */ export const isObfuscated = (user: { id: string, [key: string]: any }) => { const keys = Object.keys(user) diff --git a/apps/settings/templates/settings/admin/ai.php b/apps/settings/templates/settings/admin/ai.php new file mode 100644 index 00000000000..fac7ad8d25b --- /dev/null +++ b/apps/settings/templates/settings/admin/ai.php @@ -0,0 +1,28 @@ +<?php +/** + * @copyright Copyright (c) 2023 Marcel Klehr <mklehr@gmx.net> + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + */ + +script('settings', [ + 'vue-settings-admin-ai', +]); +?> + +<div id="ai-settings"> +</div> |