aboutsummaryrefslogtreecommitdiffstats
path: root/apps
diff options
context:
space:
mode:
Diffstat (limited to 'apps')
-rw-r--r--apps/settings/appinfo/info.xml2
-rw-r--r--apps/settings/appinfo/routes.php1
-rw-r--r--apps/settings/composer/composer/autoload_classmap.php3
-rw-r--r--apps/settings/composer/composer/autoload_static.php3
-rw-r--r--apps/settings/img/ai.svg1
-rw-r--r--apps/settings/lib/Controller/AISettingsController.php70
-rw-r--r--apps/settings/lib/Sections/Admin/ArtificialIntelligence.php58
-rw-r--r--apps/settings/lib/Settings/Admin/ArtificialIntelligence.php166
-rw-r--r--apps/settings/src/admin.js2
-rw-r--r--apps/settings/src/components/AdminAI.vue173
-rw-r--r--apps/settings/src/components/AdminDelegation/GroupSelect.vue4
-rw-r--r--apps/settings/src/components/AdminTwoFactor.vue8
-rw-r--r--apps/settings/src/components/AuthTokenSection.vue2
-rw-r--r--apps/settings/src/components/Markdown.vue2
-rw-r--r--apps/settings/src/components/PersonalInfo/AvatarSection.vue12
-rw-r--r--apps/settings/src/components/PersonalInfo/DetailsSection.vue20
-rw-r--r--apps/settings/src/components/PersonalInfo/DisplayNameSection.vue2
-rw-r--r--apps/settings/src/components/PersonalInfo/EmailSection/EmailSection.vue6
-rw-r--r--apps/settings/src/components/PersonalInfo/LanguageSection/Language.vue2
-rw-r--r--apps/settings/src/components/PersonalInfo/LocaleSection/Locale.vue4
-rw-r--r--apps/settings/src/components/PersonalInfo/shared/AccountPropertySection.vue2
-rw-r--r--apps/settings/src/components/PersonalInfo/shared/HeaderBar.vue2
-rw-r--r--apps/settings/src/components/UserList.vue4
-rw-r--r--apps/settings/src/components/Users/NewUserModal.vue32
-rw-r--r--apps/settings/src/components/Users/UserRow.vue6
-rw-r--r--apps/settings/src/main-admin-ai.js39
-rw-r--r--apps/settings/src/main-admin-security.js2
-rw-r--r--apps/settings/src/utils/userUtils.ts2
-rw-r--r--apps/settings/templates/settings/admin/ai.php28
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>&nbsp;</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>&nbsp;</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>