mirror of
https://github.com/nextcloud/server.git
synced 2026-06-07 15:53:04 -04:00
Merge pull request #39567 from nextcloud/enh/ai-admin-settings
AI admin settings
This commit is contained in:
commit
114cad3812
64 changed files with 873 additions and 95 deletions
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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' => ''],
|
||||
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
1
apps/settings/img/ai.svg
Normal file
1
apps/settings/img/ai.svg
Normal file
|
|
@ -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>
|
||||
|
After Width: | Height: | Size: 513 B |
70
apps/settings/lib/Controller/AISettingsController.php
Normal file
70
apps/settings/lib/Controller/AISettingsController.php
Normal file
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
58
apps/settings/lib/Sections/Admin/ArtificialIntelligence.php
Normal file
58
apps/settings/lib/Sections/Admin/ArtificialIntelligence.php
Normal file
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
166
apps/settings/lib/Settings/Admin/ArtificialIntelligence.php
Normal file
166
apps/settings/lib/Settings/Admin/ArtificialIntelligence.php
Normal file
|
|
@ -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..*/'],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
@ -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')
|
||||
|
|
|
|||
173
apps/settings/src/components/AdminAI.vue
Normal file
173
apps/settings/src/components/AdminAI.vue
Normal file
|
|
@ -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>
|
||||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -100,7 +100,7 @@ export default {
|
|||
'del',
|
||||
'blockquote',
|
||||
],
|
||||
}
|
||||
},
|
||||
)
|
||||
},
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -66,6 +66,6 @@ export default {
|
|||
}
|
||||
emit('settings:display-name:updated', value)
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -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'),
|
||||
{}
|
||||
{},
|
||||
)
|
||||
}
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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 }), {}),
|
||||
)
|
||||
},
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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 }), {}),
|
||||
)
|
||||
},
|
||||
},
|
||||
|
|
|
|||
|
|
@ -38,8 +38,8 @@
|
|||
autocorrect="off"
|
||||
@input="onPropertyChange" />
|
||||
<input v-else
|
||||
ref="input"
|
||||
:id="inputId"
|
||||
ref="input"
|
||||
:placeholder="placeholder"
|
||||
:type="type"
|
||||
:value="value"
|
||||
|
|
|
|||
|
|
@ -100,7 +100,7 @@ export default {
|
|||
isHeading: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
data() {
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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()),
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}"
|
||||
|
|
|
|||
39
apps/settings/src/main-admin-ai.js
Normal file
39
apps/settings/src/main-admin-ai.js
Normal file
|
|
@ -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')
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
28
apps/settings/templates/settings/admin/ai.php
Normal file
28
apps/settings/templates/settings/admin/ai.php
Normal file
|
|
@ -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>
|
||||
111
core/Migrations/Version28000Date20230728104802.php
Normal file
111
core/Migrations/Version28000Date20230728104802.php
Normal file
|
|
@ -0,0 +1,111 @@
|
|||
<?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 OC\Core\Migrations;
|
||||
|
||||
use Closure;
|
||||
use OCP\DB\ISchemaWrapper;
|
||||
use OCP\DB\Types;
|
||||
use OCP\Migration\IOutput;
|
||||
use OCP\Migration\SimpleMigrationStep;
|
||||
|
||||
/**
|
||||
* Introduce textprocessing_tasks table
|
||||
*/
|
||||
class Version28000Date20230728104802 extends SimpleMigrationStep {
|
||||
/**
|
||||
* @param IOutput $output
|
||||
* @param Closure $schemaClosure The `\Closure` returns a `ISchemaWrapper`
|
||||
* @param array $options
|
||||
* @return null|ISchemaWrapper
|
||||
*/
|
||||
public function changeSchema(IOutput $output, Closure $schemaClosure, array $options): ?ISchemaWrapper {
|
||||
/** @var ISchemaWrapper $schema */
|
||||
$schema = $schemaClosure();
|
||||
$changed = false;
|
||||
|
||||
if ($schema->hasTable('llm_tasks')) {
|
||||
$schema->dropTable('llm_tasks');
|
||||
$changed = true;
|
||||
}
|
||||
if (!$schema->hasTable('textprocessing_tasks')) {
|
||||
$table = $schema->createTable('textprocessing_tasks');
|
||||
|
||||
$table->addColumn('id', Types::BIGINT, [
|
||||
'notnull' => true,
|
||||
'length' => 64,
|
||||
'autoincrement' => true,
|
||||
]);
|
||||
$table->addColumn('type', Types::STRING, [
|
||||
'notnull' => true,
|
||||
'length' => 255,
|
||||
]);
|
||||
$table->addColumn('input', Types::TEXT, [
|
||||
'notnull' => true,
|
||||
]);
|
||||
$table->addColumn('output', Types::TEXT, [
|
||||
'notnull' => false,
|
||||
]);
|
||||
$table->addColumn('status', Types::INTEGER, [
|
||||
'notnull' => false,
|
||||
'length' => 6,
|
||||
'default' => 0,
|
||||
]);
|
||||
$table->addColumn('user_id', Types::STRING, [
|
||||
'notnull' => true,
|
||||
'length' => 64,
|
||||
]);
|
||||
$table->addColumn('app_id', Types::STRING, [
|
||||
'notnull' => true,
|
||||
'length' => 32,
|
||||
'default' => '',
|
||||
]);
|
||||
$table->addColumn('identifier', Types::STRING, [
|
||||
'notnull' => true,
|
||||
'length' => 255,
|
||||
'default' => '',
|
||||
]);
|
||||
$table->addColumn('last_updated', Types::INTEGER, [
|
||||
'notnull' => false,
|
||||
'length' => 4,
|
||||
'default' => 0,
|
||||
'unsigned' => true,
|
||||
]);
|
||||
|
||||
$table->setPrimaryKey(['id'], 'tp_tasks_id_index');
|
||||
$table->addIndex(['last_updated'], 'tp_tasks_updated');
|
||||
$table->addIndex(['status', 'type'], 'tp_tasks_status_type_nonunique');
|
||||
|
||||
$changed = true;
|
||||
}
|
||||
|
||||
if ($changed) {
|
||||
return $schema;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
4
dist/core-common.js
vendored
4
dist/core-common.js
vendored
File diff suppressed because one or more lines are too long
2
dist/core-common.js.map
vendored
2
dist/core-common.js.map
vendored
File diff suppressed because one or more lines are too long
4
dist/settings-apps-view-7418.js
vendored
4
dist/settings-apps-view-7418.js
vendored
File diff suppressed because one or more lines are too long
2
dist/settings-apps-view-7418.js.map
vendored
2
dist/settings-apps-view-7418.js.map
vendored
File diff suppressed because one or more lines are too long
2
dist/settings-legacy-admin.js.map
vendored
2
dist/settings-legacy-admin.js.map
vendored
File diff suppressed because one or more lines are too long
4
dist/settings-users-8351.js
vendored
4
dist/settings-users-8351.js
vendored
File diff suppressed because one or more lines are too long
2
dist/settings-users-8351.js.map
vendored
2
dist/settings-users-8351.js.map
vendored
File diff suppressed because one or more lines are too long
3
dist/settings-vue-settings-admin-ai.js
vendored
Normal file
3
dist/settings-vue-settings-admin-ai.js
vendored
Normal file
File diff suppressed because one or more lines are too long
30
dist/settings-vue-settings-admin-ai.js.LICENSE.txt
vendored
Normal file
30
dist/settings-vue-settings-admin-ai.js.LICENSE.txt
vendored
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
/**
|
||||
* @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/>.
|
||||
*
|
||||
*/
|
||||
|
||||
/**!
|
||||
* Sortable 1.10.2
|
||||
* @author RubaXa <trash@rubaxa.org>
|
||||
* @author owenm <owen23355@gmail.com>
|
||||
* @license MIT
|
||||
*/
|
||||
1
dist/settings-vue-settings-admin-ai.js.map
vendored
Normal file
1
dist/settings-vue-settings-admin-ai.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
4
dist/settings-vue-settings-admin-security.js
vendored
4
dist/settings-vue-settings-admin-security.js
vendored
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
4
dist/settings-vue-settings-personal-info.js
vendored
4
dist/settings-vue-settings-personal-info.js
vendored
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
|
@ -1144,6 +1144,7 @@ return array(
|
|||
'OC\\Core\\Migrations\\Version27000Date20230309104325' => $baseDir . '/core/Migrations/Version27000Date20230309104325.php',
|
||||
'OC\\Core\\Migrations\\Version27000Date20230309104802' => $baseDir . '/core/Migrations/Version27000Date20230309104802.php',
|
||||
'OC\\Core\\Migrations\\Version28000Date20230616104802' => $baseDir . '/core/Migrations/Version28000Date20230616104802.php',
|
||||
'OC\\Core\\Migrations\\Version28000Date20230728104802' => $baseDir . '/core/Migrations/Version28000Date20230728104802.php',
|
||||
'OC\\Core\\Notification\\CoreNotifier' => $baseDir . '/core/Notification/CoreNotifier.php',
|
||||
'OC\\Core\\Service\\LoginFlowV2Service' => $baseDir . '/core/Service/LoginFlowV2Service.php',
|
||||
'OC\\DB\\Adapter' => $baseDir . '/lib/private/DB/Adapter.php',
|
||||
|
|
|
|||
|
|
@ -1177,6 +1177,7 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2
|
|||
'OC\\Core\\Migrations\\Version27000Date20230309104325' => __DIR__ . '/../../..' . '/core/Migrations/Version27000Date20230309104325.php',
|
||||
'OC\\Core\\Migrations\\Version27000Date20230309104802' => __DIR__ . '/../../..' . '/core/Migrations/Version27000Date20230309104802.php',
|
||||
'OC\\Core\\Migrations\\Version28000Date20230616104802' => __DIR__ . '/../../..' . '/core/Migrations/Version28000Date20230616104802.php',
|
||||
'OC\\Core\\Migrations\\Version28000Date20230728104802' => __DIR__ . '/../../..' . '/core/Migrations/Version28000Date20230728104802.php',
|
||||
'OC\\Core\\Notification\\CoreNotifier' => __DIR__ . '/../../..' . '/core/Notification/CoreNotifier.php',
|
||||
'OC\\Core\\Service\\LoginFlowV2Service' => __DIR__ . '/../../..' . '/core/Service/LoginFlowV2Service.php',
|
||||
'OC\\DB\\Adapter' => __DIR__ . '/../../..' . '/lib/private/DB/Adapter.php',
|
||||
|
|
|
|||
|
|
@ -34,6 +34,7 @@ use OCP\BackgroundJob\IJobList;
|
|||
use OCP\Files\File;
|
||||
use OCP\Files\InvalidPathException;
|
||||
use OCP\Files\NotFoundException;
|
||||
use OCP\IConfig;
|
||||
use OCP\IServerContainer;
|
||||
use OCP\PreConditionNotMetException;
|
||||
use OCP\SpeechToText\ISpeechToTextManager;
|
||||
|
|
@ -53,6 +54,7 @@ class SpeechToTextManager implements ISpeechToTextManager {
|
|||
private Coordinator $coordinator,
|
||||
private LoggerInterface $logger,
|
||||
private IJobList $jobList,
|
||||
private IConfig $config,
|
||||
) {
|
||||
}
|
||||
|
||||
|
|
@ -111,7 +113,18 @@ class SpeechToTextManager implements ISpeechToTextManager {
|
|||
throw new PreConditionNotMetException('No SpeechToText providers have been registered');
|
||||
}
|
||||
|
||||
foreach ($this->getProviders() as $provider) {
|
||||
$providers = $this->getProviders();
|
||||
|
||||
$json = $this->config->getAppValue('core', 'ai.stt_provider', '');
|
||||
if ($json !== '') {
|
||||
$className = json_decode($json, true);
|
||||
$provider = current(array_filter($providers, fn ($provider) => $provider::class === $className));
|
||||
if ($provider !== false) {
|
||||
$providers = [$provider];
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($providers as $provider) {
|
||||
try {
|
||||
return $provider->transcribeFile($file);
|
||||
} catch (\Throwable $e) {
|
||||
|
|
|
|||
|
|
@ -41,7 +41,7 @@ class TaskMapper extends QBMapper {
|
|||
IDBConnection $db,
|
||||
private ITimeFactory $timeFactory,
|
||||
) {
|
||||
parent::__construct($db, 'llm_tasks', Task::class);
|
||||
parent::__construct($db, 'textprocessing_tasks', Task::class);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ namespace OC\TextProcessing;
|
|||
|
||||
use OC\AppFramework\Bootstrap\Coordinator;
|
||||
use OC\TextProcessing\Db\Task as DbTask;
|
||||
use OCP\IConfig;
|
||||
use OCP\TextProcessing\Task as OCPTask;
|
||||
use OC\TextProcessing\Db\TaskMapper;
|
||||
use OCP\AppFramework\Db\DoesNotExistException;
|
||||
|
|
@ -52,6 +53,7 @@ class Manager implements IManager {
|
|||
private LoggerInterface $logger,
|
||||
private IJobList $jobList,
|
||||
private TaskMapper $taskMapper,
|
||||
private IConfig $config,
|
||||
) {
|
||||
}
|
||||
|
||||
|
|
@ -111,7 +113,21 @@ class Manager implements IManager {
|
|||
if (!$this->canHandleTask($task)) {
|
||||
throw new PreConditionNotMetException('No text processing provider is installed that can handle this task');
|
||||
}
|
||||
foreach ($this->getProviders() as $provider) {
|
||||
$providers = $this->getProviders();
|
||||
$json = $this->config->getAppValue('core', 'ai.textprocessing_provider_preferences', '');
|
||||
if ($json !== '') {
|
||||
$preferences = json_decode($json, true);
|
||||
if (isset($preferences[$task->getType()])) {
|
||||
// If a preference for this task type is set, move the preferred provider to the start
|
||||
$provider = current(array_filter($providers, fn ($provider) => $provider::class === $preferences[$task->getType()]));
|
||||
if ($provider !== false) {
|
||||
$providers = array_filter($providers, fn ($p) => $p !== $provider);
|
||||
array_unshift($providers, $provider);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($providers as $provider) {
|
||||
if (!$task->canUseProvider($provider)) {
|
||||
continue;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ namespace OC\Translation;
|
|||
|
||||
use InvalidArgumentException;
|
||||
use OC\AppFramework\Bootstrap\Coordinator;
|
||||
use OCP\IConfig;
|
||||
use OCP\IServerContainer;
|
||||
use OCP\PreConditionNotMetException;
|
||||
use OCP\Translation\CouldNotTranslateException;
|
||||
|
|
@ -48,6 +49,7 @@ class TranslationManager implements ITranslationManager {
|
|||
private IServerContainer $serverContainer,
|
||||
private Coordinator $coordinator,
|
||||
private LoggerInterface $logger,
|
||||
private IConfig $config,
|
||||
) {
|
||||
}
|
||||
|
||||
|
|
@ -64,8 +66,25 @@ class TranslationManager implements ITranslationManager {
|
|||
throw new PreConditionNotMetException('No translation providers available');
|
||||
}
|
||||
|
||||
$providers = $this->getProviders();
|
||||
$json = $this->config->getAppValue('core', 'ai.translation_provider_preferences', '');
|
||||
|
||||
if ($json !== '') {
|
||||
$precedence = json_decode($json, true);
|
||||
$newProviders = [];
|
||||
foreach ($precedence as $className) {
|
||||
$provider = current(array_filter($providers, fn ($provider) => $provider::class === $className));
|
||||
if ($provider !== false) {
|
||||
$newProviders[] = $provider;
|
||||
}
|
||||
}
|
||||
// Add all providers that haven't been added so far
|
||||
$newProviders += array_udiff($providers, $newProviders, fn ($a, $b) => $a::class > $b::class ? 1 : ($a::class < $b::class ? -1 : 0));
|
||||
$providers = $newProviders;
|
||||
}
|
||||
|
||||
if ($fromLanguage === null) {
|
||||
foreach ($this->getProviders() as $provider) {
|
||||
foreach ($providers as $provider) {
|
||||
if ($provider instanceof IDetectLanguageProvider) {
|
||||
$fromLanguage = $provider->detectLanguage($text);
|
||||
}
|
||||
|
|
@ -84,11 +103,11 @@ class TranslationManager implements ITranslationManager {
|
|||
return $text;
|
||||
}
|
||||
|
||||
foreach ($this->getProviders() as $provider) {
|
||||
foreach ($providers as $provider) {
|
||||
try {
|
||||
return $provider->translate($fromLanguage, $toLanguage, $text);
|
||||
} catch (RuntimeException $e) {
|
||||
$this->logger->warning("Failed to translate from {$fromLanguage} to {$toLanguage}", ['exception' => $e]);
|
||||
$this->logger->warning("Failed to translate from {$fromLanguage} to {$toLanguage} using provider {$provider->getName()}", ['exception' => $e]);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -40,6 +40,12 @@ interface ISpeechToTextManager {
|
|||
*/
|
||||
public function hasProviders(): bool;
|
||||
|
||||
/**
|
||||
* @return ISpeechToTextProvider[]
|
||||
* @since 27.1.0
|
||||
*/
|
||||
public function getProviders(): array;
|
||||
|
||||
/**
|
||||
* Will schedule a transcription process in the background. The result will become available
|
||||
* with the \OCP\SpeechToText\Events\TranscriptionFinishedEvent
|
||||
|
|
|
|||
|
|
@ -26,21 +26,25 @@ declare(strict_types=1);
|
|||
namespace OCP\TextProcessing;
|
||||
|
||||
use OCP\IL10N;
|
||||
use OCP\L10N\IFactory;
|
||||
|
||||
/**
|
||||
* This is the text processing task type for free prompting
|
||||
* @since 27.1.0
|
||||
*/
|
||||
class FreePromptTaskType implements ITaskType {
|
||||
private IL10N $l;
|
||||
|
||||
/**
|
||||
* Constructor for FreePromptTaskType
|
||||
*
|
||||
* @param IL10N $l
|
||||
* @param IFactory $l10nFactory
|
||||
* @since 27.1.0
|
||||
*/
|
||||
public function __construct(
|
||||
private IL10N $l,
|
||||
IFactory $l10nFactory,
|
||||
) {
|
||||
$this->l = $l10nFactory->get('core');
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -26,21 +26,25 @@ declare(strict_types=1);
|
|||
namespace OCP\TextProcessing;
|
||||
|
||||
use OCP\IL10N;
|
||||
use OCP\L10N\IFactory;
|
||||
|
||||
/**
|
||||
* This is the text processing task type for creating headline
|
||||
* @since 27.1.0
|
||||
*/
|
||||
class HeadlineTaskType implements ITaskType {
|
||||
private IL10N $l;
|
||||
|
||||
/**
|
||||
* Constructor for HeadlineTaskType
|
||||
*
|
||||
* @param IL10N $l
|
||||
* @param IFactory $l10nFactory
|
||||
* @since 27.1.0
|
||||
*/
|
||||
public function __construct(
|
||||
private IL10N $l,
|
||||
IFactory $l10nFactory,
|
||||
) {
|
||||
$this->l = $l10nFactory->get('core');
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -57,6 +61,6 @@ class HeadlineTaskType implements ITaskType {
|
|||
* @since 27.1.0
|
||||
*/
|
||||
public function getDescription(): string {
|
||||
return $this->l->t('Generates a possible headline for a text');
|
||||
return $this->l->t('Generates a possible headline for a text.');
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -41,6 +41,12 @@ interface IManager {
|
|||
*/
|
||||
public function hasProviders(): bool;
|
||||
|
||||
/**
|
||||
* @return IProvider[]
|
||||
* @since 27.1.0
|
||||
*/
|
||||
public function getProviders(): array;
|
||||
|
||||
/**
|
||||
* @return class-string<ITaskType>[]
|
||||
* @since 27.1.0
|
||||
|
|
|
|||
|
|
@ -26,21 +26,25 @@ declare(strict_types=1);
|
|||
namespace OCP\TextProcessing;
|
||||
|
||||
use OCP\IL10N;
|
||||
use OCP\L10N\IFactory;
|
||||
|
||||
/**
|
||||
* This is the text processing task type for summaries
|
||||
* @since 27.1.0
|
||||
*/
|
||||
class SummaryTaskType implements ITaskType {
|
||||
private IL10N $l;
|
||||
|
||||
/**
|
||||
* Constructor for SummaryTaskType
|
||||
*
|
||||
* @param IL10N $l
|
||||
* @param IFactory $l10nFactory
|
||||
* @since 27.1.0
|
||||
*/
|
||||
public function __construct(
|
||||
private IL10N $l,
|
||||
IFactory $l10nFactory,
|
||||
) {
|
||||
$this->l = $l10nFactory->get('core');
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -26,21 +26,25 @@ declare(strict_types=1);
|
|||
namespace OCP\TextProcessing;
|
||||
|
||||
use OCP\IL10N;
|
||||
use OCP\L10N\IFactory;
|
||||
|
||||
/**
|
||||
* This is the text processing task type for topics extraction
|
||||
* @since 27.1.0
|
||||
*/
|
||||
class TopicsTaskType implements ITaskType {
|
||||
private IL10N $l;
|
||||
|
||||
/**
|
||||
* Constructor for TopicsTaskType
|
||||
*
|
||||
* @param IL10N $l
|
||||
* @param IFactory $l10nFactory
|
||||
* @since 27.1.0
|
||||
*/
|
||||
public function __construct(
|
||||
private IL10N $l,
|
||||
IFactory $l10nFactory,
|
||||
) {
|
||||
$this->l = $l10nFactory->get('core');
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -38,6 +38,12 @@ interface ITranslationManager {
|
|||
*/
|
||||
public function hasProviders(): bool;
|
||||
|
||||
/**
|
||||
* @return ITranslationProvider[]
|
||||
* @since 27.1.0
|
||||
*/
|
||||
public function getProviders(): array;
|
||||
|
||||
/**
|
||||
* @since 26.0.0
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -82,6 +82,7 @@ module.exports = {
|
|||
apps: path.join(__dirname, 'apps/settings/src', 'apps.js'),
|
||||
'legacy-admin': path.join(__dirname, 'apps/settings/src', 'admin.js'),
|
||||
'vue-settings-admin-basic-settings': path.join(__dirname, 'apps/settings/src', 'main-admin-basic-settings.js'),
|
||||
'vue-settings-admin-ai': path.join(__dirname, 'apps/settings/src', 'main-admin-ai.js'),
|
||||
'vue-settings-admin-delegation': path.join(__dirname, 'apps/settings/src', 'main-admin-delegation.js'),
|
||||
'vue-settings-admin-security': path.join(__dirname, 'apps/settings/src', 'main-admin-security.js'),
|
||||
'vue-settings-apps-users-management': path.join(__dirname, 'apps/settings/src', 'main-apps-users-management.js'),
|
||||
|
|
|
|||
Loading…
Reference in a new issue