add thread summary ui
Signed-off-by: hamza221 <hamzamahjoubi221@gmail.com>
This commit is contained in:
Родитель
5066737482
Коммит
0cb4ed5e8c
|
@ -320,6 +320,11 @@ return [
|
|||
'url' => '/api/settings/allownewaccounts',
|
||||
'verb' => 'POST'
|
||||
],
|
||||
[
|
||||
'name' => 'settings#setEnabledThreadSummary',
|
||||
'url' => '/api/settings/threadsummary',
|
||||
'verb' => 'PUT'
|
||||
],
|
||||
[
|
||||
'name' => 'trusted_senders#setTrusted',
|
||||
'url' => '/api/trustedsenders/{email}',
|
||||
|
@ -360,6 +365,11 @@ return [
|
|||
'url' => '/api/thread/{id}',
|
||||
'verb' => 'POST'
|
||||
],
|
||||
[
|
||||
'name' => 'thread#summarize',
|
||||
'url' => '/api/thread/{id}/summary',
|
||||
'verb' => 'GET'
|
||||
],
|
||||
[
|
||||
'name' => 'outbox#send',
|
||||
'url' => '/api/outbox/{id}',
|
||||
|
|
|
@ -33,6 +33,7 @@ use OCA\Mail\Contracts\IUserPreferences;
|
|||
use OCA\Mail\Db\SmimeCertificate;
|
||||
use OCA\Mail\Db\TagMapper;
|
||||
use OCA\Mail\Service\AccountService;
|
||||
use OCA\Mail\Service\AiIntegrationsService;
|
||||
use OCA\Mail\Service\AliasesService;
|
||||
use OCA\Mail\Service\OutboxService;
|
||||
use OCA\Mail\Service\SmimeService;
|
||||
|
@ -73,6 +74,7 @@ class PageController extends Controller {
|
|||
private IEventDispatcher $dispatcher;
|
||||
private ICredentialstore $credentialStore;
|
||||
private SmimeService $smimeService;
|
||||
private AiIntegrationsService $aiIntegrationsService;
|
||||
|
||||
public function __construct(string $appName,
|
||||
IRequest $request,
|
||||
|
@ -90,7 +92,8 @@ class PageController extends Controller {
|
|||
OutboxService $outboxService,
|
||||
IEventDispatcher $dispatcher,
|
||||
ICredentialStore $credentialStore,
|
||||
SmimeService $smimeService) {
|
||||
SmimeService $smimeService,
|
||||
AiIntegrationsService $aiIntegrationsService) {
|
||||
parent::__construct($appName, $request);
|
||||
|
||||
$this->urlGenerator = $urlGenerator;
|
||||
|
@ -108,6 +111,7 @@ class PageController extends Controller {
|
|||
$this->dispatcher = $dispatcher;
|
||||
$this->credentialStore = $credentialStore;
|
||||
$this->smimeService = $smimeService;
|
||||
$this->aiIntegrationsService = $aiIntegrationsService;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -244,6 +248,11 @@ class PageController extends Controller {
|
|||
$this->config->getAppValue('mail', 'allow_new_mail_accounts', 'yes') === 'yes'
|
||||
);
|
||||
|
||||
$this->initialStateService->provideInitialState(
|
||||
'enabled_thread_summary',
|
||||
$this->config->getAppValue('mail', 'enabled_thread_summary', 'no') === 'yes' && $this->aiIntegrationsService->isLlmAvailable()
|
||||
);
|
||||
|
||||
$this->initialStateService->provideInitialState(
|
||||
'smime-certificates',
|
||||
array_map(
|
||||
|
|
|
@ -34,22 +34,29 @@ use OCP\AppFramework\Controller;
|
|||
use OCP\AppFramework\Http\JSONResponse;
|
||||
use OCP\IConfig;
|
||||
use OCP\IRequest;
|
||||
use OCP\TextProcessing\IManager;
|
||||
use OCP\TextProcessing\SummaryTaskType;
|
||||
use Psr\Container\ContainerInterface;
|
||||
|
||||
use function array_merge;
|
||||
|
||||
class SettingsController extends Controller {
|
||||
private ProvisioningManager $provisioningManager;
|
||||
private AntiSpamService $antiSpamService;
|
||||
private ContainerInterface $container;
|
||||
|
||||
private IConfig $config;
|
||||
|
||||
public function __construct(IRequest $request,
|
||||
ProvisioningManager $provisioningManager,
|
||||
AntiSpamService $antiSpamService,
|
||||
IConfig $config) {
|
||||
IConfig $config,
|
||||
ContainerInterface $container) {
|
||||
parent::__construct(Application::APP_ID, $request);
|
||||
$this->provisioningManager = $provisioningManager;
|
||||
$this->antiSpamService = $antiSpamService;
|
||||
$this->config = $config;
|
||||
$this->container = $container;
|
||||
}
|
||||
|
||||
public function index(): JSONResponse {
|
||||
|
@ -119,4 +126,17 @@ class SettingsController extends Controller {
|
|||
public function setAllowNewMailAccounts(bool $allowed) {
|
||||
$this->config->setAppValue('mail', 'allow_new_mail_accounts', $allowed ? 'yes' : 'no');
|
||||
}
|
||||
|
||||
public function setEnabledThreadSummary(bool $enabled) {
|
||||
$this->config->setAppValue('mail', 'enabled_thread_summary', $enabled ? 'yes' : 'no');
|
||||
}
|
||||
|
||||
public function isLlmConfigured() {
|
||||
try {
|
||||
$manager = $this->container->get(IManager::class);
|
||||
} catch (\Throwable $e) {
|
||||
return new JSONResponse(['data' => false]);
|
||||
}
|
||||
return new JSONResponse(['data' => in_array(SummaryTaskType::class, $manager->getAvailableTaskTypes(), true)]);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -28,27 +28,35 @@ use OCA\Mail\Exception\ClientException;
|
|||
use OCA\Mail\Exception\ServiceException;
|
||||
use OCA\Mail\Http\TrapError;
|
||||
use OCA\Mail\Service\AccountService;
|
||||
use OCA\Mail\Service\AiIntegrationsService;
|
||||
use OCP\AppFramework\Controller;
|
||||
use OCP\AppFramework\Db\DoesNotExistException;
|
||||
use OCP\AppFramework\Http;
|
||||
use OCP\AppFramework\Http\JSONResponse;
|
||||
use OCP\IRequest;
|
||||
use Psr\Log\LoggerInterface;
|
||||
|
||||
class ThreadController extends Controller {
|
||||
private string $currentUserId;
|
||||
private AccountService $accountService;
|
||||
private IMailManager $mailManager;
|
||||
private AiIntegrationsService $aiIntergrationsService;
|
||||
private LoggerInterface $logger;
|
||||
|
||||
|
||||
public function __construct(string $appName,
|
||||
IRequest $request,
|
||||
string $UserId,
|
||||
AccountService $accountService,
|
||||
IMailManager $mailManager) {
|
||||
IMailManager $mailManager,
|
||||
AiIntegrationsService $aiIntergrationsService,
|
||||
LoggerInterface $logger) {
|
||||
parent::__construct($appName, $request);
|
||||
|
||||
$this->currentUserId = $UserId;
|
||||
$this->accountService = $accountService;
|
||||
$this->mailManager = $mailManager;
|
||||
$this->aiIntergrationsService = $aiIntergrationsService;
|
||||
$this->logger = $logger;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -111,4 +119,40 @@ class ThreadController extends Controller {
|
|||
|
||||
return new JSONResponse();
|
||||
}
|
||||
|
||||
/**
|
||||
* @NoAdminRequired
|
||||
*
|
||||
* @param int $id
|
||||
*
|
||||
* @return JSONResponse
|
||||
*/
|
||||
public function summarize(int $id): JSONResponse {
|
||||
try {
|
||||
$message = $this->mailManager->getMessage($this->currentUserId, $id);
|
||||
$mailbox = $this->mailManager->getMailbox($this->currentUserId, $message->getMailboxId());
|
||||
$account = $this->accountService->find($this->currentUserId, $mailbox->getAccountId());
|
||||
} catch (DoesNotExistException $e) {
|
||||
return new JSONResponse([], Http::STATUS_FORBIDDEN);
|
||||
}
|
||||
if (empty($message->getThreadRootId())) {
|
||||
return new JSONResponse([], Http::STATUS_NOT_FOUND);
|
||||
}
|
||||
$thread = $this->mailManager->getThread($account, $message->getThreadRootId());
|
||||
try {
|
||||
$summary = $this->aiIntergrationsService->summarizeThread(
|
||||
$message->getThreadRootId(),
|
||||
$thread,
|
||||
$this->currentUserId,
|
||||
);
|
||||
} catch (\Throwable $e) {
|
||||
$this->logger->error('Summarizing thread failed: ' . $e->getMessage(), [
|
||||
'exception' => $e,
|
||||
]);
|
||||
return new JSONResponse([], Http::STATUS_NO_CONTENT);
|
||||
}
|
||||
|
||||
return new JSONResponse(['data' => $summary]);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -0,0 +1,80 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* @author Hamza Mahjoubi <hamzamahjoubi22@proton.me>
|
||||
*
|
||||
* Mail
|
||||
*
|
||||
* 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\Mail\Service;
|
||||
|
||||
use OCA\Mail\Exception\ServiceException;
|
||||
use OCP\TextProcessing\IManager;
|
||||
use OCP\TextProcessing\SummaryTaskType;
|
||||
use OCP\TextProcessing\Task;
|
||||
use Psr\Container\ContainerInterface;
|
||||
use function array_map;
|
||||
|
||||
class AiIntegrationsService {
|
||||
|
||||
/** @var ContainerInterface */
|
||||
private ContainerInterface $container;
|
||||
|
||||
|
||||
public function __construct(ContainerInterface $container) {
|
||||
$this->container = $container;
|
||||
}
|
||||
/**
|
||||
* @param string $threadId
|
||||
* @param array $messages
|
||||
* @param string $currentUserId
|
||||
*
|
||||
* @return null|string
|
||||
*
|
||||
* @throws ServiceException
|
||||
*/
|
||||
public function summarizeThread(string $threadId, array $messages, string $currentUserId): null|string {
|
||||
try {
|
||||
$manager = $this->container->get(IManager::class);
|
||||
} catch (\Throwable $e) {
|
||||
throw new ServiceException('Text processing is not available in your current Nextcloud version', $e);
|
||||
}
|
||||
if(in_array(SummaryTaskType::class, $manager->getAvailableTaskTypes(), true)) {
|
||||
$messagesBodies = array_map(function ($message) {
|
||||
return $message->getPreviewText();
|
||||
}, $messages);
|
||||
|
||||
$taskPrompt = implode("\n", $messagesBodies);
|
||||
$summaryTask = new Task(SummaryTaskType::class, $taskPrompt, "mail", $currentUserId, $threadId);
|
||||
$manager->runTask($summaryTask);
|
||||
|
||||
return $summaryTask->getOutput();
|
||||
} else {
|
||||
throw new ServiceException('No language model available for summary');
|
||||
}
|
||||
}
|
||||
|
||||
public function isLlmAvailable(): bool {
|
||||
try {
|
||||
$manager = $this->container->get(IManager::class);
|
||||
} catch (\Throwable $e) {
|
||||
return false;
|
||||
}
|
||||
return in_array(SummaryTaskType::class, $manager->getAvailableTaskTypes(), true);
|
||||
}
|
||||
}
|
|
@ -28,6 +28,7 @@ namespace OCA\Mail\Settings;
|
|||
use OCA\Mail\AppInfo\Application;
|
||||
use OCA\Mail\Integration\GoogleIntegration;
|
||||
use OCA\Mail\Integration\MicrosoftIntegration;
|
||||
use OCA\Mail\Service\AiIntegrationsService;
|
||||
use OCA\Mail\Service\AntiSpamService;
|
||||
use OCA\Mail\Service\Provisioning\Manager as ProvisioningManager;
|
||||
use OCP\AppFramework\Http\TemplateResponse;
|
||||
|
@ -49,19 +50,22 @@ class AdminSettings implements ISettings {
|
|||
private GoogleIntegration $googleIntegration;
|
||||
private MicrosoftIntegration $microsoftIntegration;
|
||||
private IConfig $config;
|
||||
private AiIntegrationsService $aiIntegrationsService;
|
||||
|
||||
public function __construct(IInitialStateService $initialStateService,
|
||||
ProvisioningManager $provisioningManager,
|
||||
AntiSpamService $antiSpamService,
|
||||
GoogleIntegration $googleIntegration,
|
||||
MicrosoftIntegration $microsoftIntegration,
|
||||
IConfig $config) {
|
||||
IConfig $config,
|
||||
AiIntegrationsService $aiIntegrationsService) {
|
||||
$this->initialStateService = $initialStateService;
|
||||
$this->provisioningManager = $provisioningManager;
|
||||
$this->antiSpamService = $antiSpamService;
|
||||
$this->googleIntegration = $googleIntegration;
|
||||
$this->microsoftIntegration = $microsoftIntegration;
|
||||
$this->config = $config;
|
||||
$this->aiIntegrationsService = $aiIntegrationsService;
|
||||
}
|
||||
|
||||
public function getForm() {
|
||||
|
@ -85,6 +89,19 @@ class AdminSettings implements ISettings {
|
|||
'allow_new_mail_accounts',
|
||||
$this->config->getAppValue('mail', 'allow_new_mail_accounts', 'yes') === 'yes'
|
||||
);
|
||||
|
||||
$this->initialStateService->provideInitialState(
|
||||
Application::APP_ID,
|
||||
'enabled_thread_summary',
|
||||
$this->config->getAppValue('mail', 'enabled_thread_summary', 'no') === 'yes'
|
||||
);
|
||||
|
||||
$this->initialStateService->provideInitialState(
|
||||
Application::APP_ID,
|
||||
'enabled_llm_backend',
|
||||
$this->aiIntegrationsService->isLlmAvailable()
|
||||
);
|
||||
|
||||
$this->initialStateService->provideLazyInitialState(
|
||||
Application::APP_ID,
|
||||
'ldap_aliases_integration',
|
||||
|
|
|
@ -42,6 +42,10 @@
|
|||
<referencedClass name="Symfony\Component\Console\Input\InputInterface" />
|
||||
<referencedClass name="Symfony\Component\Console\Input\InputOption" />
|
||||
<referencedClass name="Symfony\Component\Console\Output\OutputInterface" />
|
||||
<referencedClass name="OCP\TextProcessing\IManager" />
|
||||
<referencedClass name="OCP\TextProcessing\SummaryTaskType" />
|
||||
<referencedClass name="OCP\TextProcessing\Task" />
|
||||
<referencedClass name="OCP\TextProcessing\SummaryTaskType" />
|
||||
</errorLevel>
|
||||
</UndefinedClass>
|
||||
<UndefinedDocblockClass>
|
||||
|
|
|
@ -38,6 +38,7 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<ThreadSummary v-if="showSummaryBox" :loading="summaryLoading" :summary="summaryText" />
|
||||
<ThreadEnvelope v-for="env in thread"
|
||||
:key="env.databaseId"
|
||||
:envelope="env"
|
||||
|
@ -54,21 +55,26 @@
|
|||
|
||||
<script>
|
||||
import { NcAppContentDetails as AppContentDetails, NcPopover as Popover } from '@nextcloud/vue'
|
||||
import { showError } from '@nextcloud/dialogs'
|
||||
|
||||
import { prop, uniqBy } from 'ramda'
|
||||
import debounce from 'lodash/fp/debounce'
|
||||
import { loadState } from '@nextcloud/initial-state'
|
||||
|
||||
import { summarizeThread } from '../service/AiIntergrationsService'
|
||||
import { getRandomMessageErrorMessage } from '../util/ErrorMessageFactory'
|
||||
import Loading from './Loading'
|
||||
import logger from '../logger'
|
||||
import Error from './Error'
|
||||
import RecipientBubble from './RecipientBubble'
|
||||
import ThreadEnvelope from './ThreadEnvelope'
|
||||
import ThreadSummary from './ThreadSummary'
|
||||
|
||||
export default {
|
||||
name: 'Thread',
|
||||
components: {
|
||||
RecipientBubble,
|
||||
ThreadSummary,
|
||||
AppContentDetails,
|
||||
Error,
|
||||
Loading,
|
||||
|
@ -78,6 +84,7 @@ export default {
|
|||
|
||||
data() {
|
||||
return {
|
||||
summaryLoading: false,
|
||||
loading: true,
|
||||
message: undefined,
|
||||
errorMessage: '',
|
||||
|
@ -85,6 +92,9 @@ export default {
|
|||
expandedThreads: [],
|
||||
participantsToDisplay: 999,
|
||||
resizeDebounced: debounce(500, this.updateParticipantsToDisplay),
|
||||
enabledThreadSummary: loadState('mail', 'enabled_thread_summary', false),
|
||||
summaryText: '',
|
||||
summaryError: false,
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -134,6 +144,9 @@ export default {
|
|||
}
|
||||
return thread[0].subject || this.t('mail', 'No subject')
|
||||
},
|
||||
showSummaryBox() {
|
||||
return this.thread.length > 2 && this.enabledThreadSummary && !this.summaryError
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
$route(to, from) {
|
||||
|
@ -158,6 +171,20 @@ export default {
|
|||
window.removeEventListener('resize', this.resizeDebounced)
|
||||
},
|
||||
methods: {
|
||||
async updateSummary() {
|
||||
if (this.thread.length < 2 || !this.enabledThreadSummary) return
|
||||
|
||||
this.summaryLoading = true
|
||||
try {
|
||||
this.summaryText = await summarizeThread(this.thread[0].databaseId)
|
||||
} catch (error) {
|
||||
this.summaryError = true
|
||||
showError(t('mail', 'Summarizing thread failed.'))
|
||||
logger.error('Summarizing thread failed', { error })
|
||||
} finally {
|
||||
this.summaryLoading = false
|
||||
}
|
||||
},
|
||||
updateParticipantsToDisplay() {
|
||||
// Wait until everything is in place
|
||||
if (!this.$refs.avatarHeader || !this.threadParticipants) {
|
||||
|
@ -226,6 +253,7 @@ export default {
|
|||
this.error = undefined
|
||||
await this.fetchThread()
|
||||
this.updateParticipantsToDisplay()
|
||||
this.updateSummary()
|
||||
|
||||
},
|
||||
async fetchThread() {
|
||||
|
|
|
@ -0,0 +1,107 @@
|
|||
<template>
|
||||
<div class="summary">
|
||||
<div class="summary__header">
|
||||
<div class="summary__header__title">
|
||||
<span class="summary__header__title__brand">
|
||||
<CreationIcon class="summary__header__title__brand__icon" />
|
||||
<p>{{ brand }}</p>
|
||||
</span>
|
||||
|
||||
<h2>{{ t('mail', 'Thread Summary') }}</h2>
|
||||
</div>
|
||||
<div class="summary__header__actions">
|
||||
<NcActions />
|
||||
<NcButton
|
||||
:aria-label=" t('mail', 'Go to latest message')"
|
||||
type="secondary"
|
||||
@click="onScroll">
|
||||
{{ t('mail', 'Go to newest message') }}
|
||||
<template #icon>
|
||||
<ArrowDownIcon
|
||||
:size="20" />
|
||||
</template>
|
||||
</NcButton>
|
||||
</div>
|
||||
</div>
|
||||
<div class="summary__body">
|
||||
<LoadingSkeleton v-if="loading" :number-of-lines="1" :with-avatar="false" />
|
||||
<p v-else>
|
||||
{{ summary }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import NcButton from '@nextcloud/vue/dist/Components/NcButton'
|
||||
import NcActions from '@nextcloud/vue/dist/Components/NcActions'
|
||||
import CreationIcon from 'vue-material-design-icons/Creation'
|
||||
import ArrowDownIcon from 'vue-material-design-icons/ArrowDown'
|
||||
import LoadingSkeleton from './LoadingSkeleton'
|
||||
|
||||
export default {
|
||||
name: 'ThreadSummary',
|
||||
components: {
|
||||
LoadingSkeleton,
|
||||
NcButton,
|
||||
NcActions,
|
||||
CreationIcon,
|
||||
ArrowDownIcon,
|
||||
},
|
||||
props: {
|
||||
summary: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
loading: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
brand() {
|
||||
if (OCA.Theming) {
|
||||
return t('mail', '{name} Assistant', { name: OCA.Theming.name })
|
||||
}
|
||||
return t('mail', '{name} Assistant', { name: 'Nextcloud' })
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
onScroll() {
|
||||
const pane = document.querySelector('.splitpanes__pane-details')
|
||||
pane.scrollTo({ top: pane.scrollHeight, left: 0, behavior: 'smooth' })
|
||||
},
|
||||
},
|
||||
|
||||
}
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.summary{
|
||||
border: 2px solid var(--color-primary-element);
|
||||
border-radius:var( --border-radius-large) ;
|
||||
margin: 0 10px 20px 10px;
|
||||
padding: 28px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
&__header{
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
&__title{
|
||||
&__brand{
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background-color: var(--color-primary-light);
|
||||
border-radius: var(--border-radius-pill);
|
||||
width: fit-content;
|
||||
padding-right: 5px;
|
||||
|
||||
&__icon{
|
||||
color:var(--color-primary-element);
|
||||
margin-right: 5px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -150,6 +150,20 @@
|
|||
</p>
|
||||
</article>
|
||||
</div>
|
||||
<div v-if="isLlmConfigured"
|
||||
class="app-description">
|
||||
<h3>{{ t('mail', 'Enable thread summary') }}</h3>
|
||||
<article>
|
||||
<p>
|
||||
<NcCheckboxRadioSwitch
|
||||
:checked.sync="enabledThreadSummary"
|
||||
type="switch"
|
||||
@update:checked="updateEnabledThreadSummary">
|
||||
{{ t('mail','Enable thread summaries') }}
|
||||
</NcCheckboxRadioSwitch>
|
||||
</p>
|
||||
</article>
|
||||
</div>
|
||||
<div class="app-description">
|
||||
<h3>
|
||||
{{
|
||||
|
@ -264,7 +278,7 @@ import {
|
|||
updateProvisioningSettings,
|
||||
provisionAll,
|
||||
updateAllowNewMailAccounts,
|
||||
|
||||
updateEnabledThreadSummary,
|
||||
} from '../../service/SettingsService'
|
||||
|
||||
const googleOauthClientId = loadState('mail', 'google_oauth_client_id', null) ?? undefined
|
||||
|
@ -324,6 +338,8 @@ export default {
|
|||
loading: false,
|
||||
},
|
||||
allowNewMailAccounts: loadState('mail', 'allow_new_mail_accounts', true),
|
||||
enabledThreadSummary: loadState('mail', 'enabled_thread_summary', false),
|
||||
isLlmConfigured: loadState('mail', 'enabled_llm_backend'),
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
|
@ -377,6 +393,9 @@ export default {
|
|||
async updateAllowNewMailAccounts(checked) {
|
||||
await updateAllowNewMailAccounts(checked)
|
||||
},
|
||||
async updateEnabledThreadSummary(checked) {
|
||||
await updateEnabledThreadSummary(checked)
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -0,0 +1,17 @@
|
|||
import { generateUrl } from '@nextcloud/router'
|
||||
import axios from '@nextcloud/axios'
|
||||
import { convertAxiosError } from '../errors/convert'
|
||||
|
||||
export const summarizeThread = async (threadId) => {
|
||||
const url = generateUrl('/apps/mail/api/thread/{threadId}/summary', {
|
||||
threadId,
|
||||
})
|
||||
|
||||
try {
|
||||
const resp = await axios.get(url)
|
||||
if (resp.status === 204) throw convertAxiosError()
|
||||
return resp.data.data
|
||||
} catch (e) {
|
||||
throw convertAxiosError(e)
|
||||
}
|
||||
}
|
|
@ -76,3 +76,12 @@ export const updateAllowNewMailAccounts = (allowed) => {
|
|||
}
|
||||
return axios.post(url, data).then((resp) => resp.data)
|
||||
}
|
||||
|
||||
export const updateEnabledThreadSummary = async (enabled) => {
|
||||
const url = generateUrl('/apps/mail/api/settings/threadsummary')
|
||||
const data = {
|
||||
enabled,
|
||||
}
|
||||
const resp = await axios.put(url, data)
|
||||
return resp.data
|
||||
}
|
||||
|
|
|
@ -31,6 +31,7 @@ use OCA\Mail\Controller\PageController;
|
|||
use OCA\Mail\Db\Mailbox;
|
||||
use OCA\Mail\Db\TagMapper;
|
||||
use OCA\Mail\Service\AccountService;
|
||||
use OCA\Mail\Service\AiIntegrationsService;
|
||||
use OCA\Mail\Service\AliasesService;
|
||||
use OCA\Mail\Service\MailManager;
|
||||
use OCA\Mail\Service\OutboxService;
|
||||
|
@ -69,6 +70,9 @@ class PageControllerTest extends TestCase {
|
|||
/** @var AccountService|MockObject */
|
||||
private $accountService;
|
||||
|
||||
/** @var AiIntegrationsService|MockObject */
|
||||
private $aiIntegrationsService;
|
||||
|
||||
/** @var AliasesService|MockObject */
|
||||
private $aliasesService;
|
||||
|
||||
|
@ -113,6 +117,7 @@ class PageControllerTest extends TestCase {
|
|||
$this->urlGenerator = $this->createMock(IURLGenerator::class);
|
||||
$this->config = $this->createMock(IConfig::class);
|
||||
$this->accountService = $this->createMock(AccountService::class);
|
||||
$this->aiIntegrationsService = $this->createMock(AiIntegrationsService::class);
|
||||
$this->aliasesService = $this->createMock(AliasesService::class);
|
||||
$this->userSession = $this->createMock(IUserSession::class);
|
||||
$this->preferences = $this->createMock(IUserPreferences::class);
|
||||
|
@ -143,6 +148,7 @@ class PageControllerTest extends TestCase {
|
|||
$this->eventDispatcher,
|
||||
$this->credentialStore,
|
||||
$this->smimeService,
|
||||
$this->aiIntegrationsService,
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -231,7 +237,7 @@ class PageControllerTest extends TestCase {
|
|||
['version', '0.0.0', '26.0.0'],
|
||||
['app.mail.attachment-size-limit', 0, 123],
|
||||
]);
|
||||
$this->config->expects($this->exactly(6))
|
||||
$this->config->expects($this->exactly(7))
|
||||
->method('getAppValue')
|
||||
->withConsecutive(
|
||||
[ 'mail', 'installed_version' ],
|
||||
|
@ -239,14 +245,16 @@ class PageControllerTest extends TestCase {
|
|||
['mail', 'microsoft_oauth_client_id' ],
|
||||
['mail', 'microsoft_oauth_tenant_id' ],
|
||||
['core', 'backgroundjobs_mode', 'ajax' ],
|
||||
['mail', 'allow_new_mail_accounts', 'yes']
|
||||
['mail', 'allow_new_mail_accounts', 'yes'],
|
||||
['mail', 'enabled_thread_summary', 'no'],
|
||||
)->willReturnOnConsecutiveCalls(
|
||||
$this->returnValue('1.2.3'),
|
||||
$this->returnValue(''),
|
||||
$this->returnValue(''),
|
||||
$this->returnValue(''),
|
||||
$this->returnValue('cron'),
|
||||
$this->returnValue('yes')
|
||||
$this->returnValue('yes'),
|
||||
$this->returnValue('no')
|
||||
);
|
||||
$user->expects($this->once())
|
||||
->method('getDisplayName')
|
||||
|
@ -267,7 +275,7 @@ class PageControllerTest extends TestCase {
|
|||
->method('getLoginCredentials')
|
||||
->willReturn($loginCredentials);
|
||||
|
||||
$this->initialState->expects($this->exactly(12))
|
||||
$this->initialState->expects($this->exactly(13))
|
||||
->method('provideInitialState')
|
||||
->withConsecutive(
|
||||
['debug', true],
|
||||
|
@ -281,6 +289,7 @@ class PageControllerTest extends TestCase {
|
|||
['outbox-messages', []],
|
||||
['disable-scheduled-send', false],
|
||||
['allow-new-accounts', true],
|
||||
['enabled_thread_summary', false],
|
||||
['smime-certificates', []],
|
||||
);
|
||||
|
||||
|
|
|
@ -31,10 +31,12 @@ use OCA\Mail\Db\MailAccount;
|
|||
use OCA\Mail\Db\Mailbox;
|
||||
use OCA\Mail\Db\Message;
|
||||
use OCA\Mail\Service\AccountService;
|
||||
use OCA\Mail\Service\AiIntegrationsService;
|
||||
use OCP\AppFramework\Db\DoesNotExistException;
|
||||
use OCP\AppFramework\Http;
|
||||
use OCP\IRequest;
|
||||
use PHPUnit\Framework\MockObject\MockObject;
|
||||
use Psr\Log\LoggerInterface;
|
||||
|
||||
class ThreadControllerTest extends TestCase {
|
||||
/** @var string */
|
||||
|
@ -49,12 +51,19 @@ class ThreadControllerTest extends TestCase {
|
|||
/** @var AccountService|MockObject */
|
||||
private $accountService;
|
||||
|
||||
/** @var AiIntegrationsService|MockObject */
|
||||
private $aiIntergrationsService;
|
||||
|
||||
|
||||
/** @var IMailManager|MockObject */
|
||||
private $mailManager;
|
||||
|
||||
/** @var ThreadController */
|
||||
private $controller;
|
||||
|
||||
/** @var LoggerInterface|MockObject */
|
||||
private $logger;
|
||||
|
||||
protected function setUp(): void {
|
||||
parent::setUp();
|
||||
|
||||
|
@ -62,14 +71,18 @@ class ThreadControllerTest extends TestCase {
|
|||
$this->request = $this->getMockBuilder(IRequest::class)->getMock();
|
||||
$this->userId = 'john';
|
||||
$this->accountService = $this->createMock(AccountService::class);
|
||||
$this->aiIntergrationsService = $this->createMock(AiIntegrationsService::class);
|
||||
$this->mailManager = $this->createMock(IMailManager::class);
|
||||
$this->logger = $this->createMock(LoggerInterface::class);
|
||||
|
||||
$this->controller = new ThreadController(
|
||||
$this->appName,
|
||||
$this->request,
|
||||
$this->userId,
|
||||
$this->accountService,
|
||||
$this->mailManager
|
||||
$this->mailManager,
|
||||
$this->aiIntergrationsService,
|
||||
$this->logger
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -53,7 +53,7 @@ class AdminSettingsTest extends TestCase {
|
|||
}
|
||||
|
||||
public function testGetForm() {
|
||||
$this->serviceMock->getParameter('initialStateService')->expects($this->exactly(8))
|
||||
$this->serviceMock->getParameter('initialStateService')->expects($this->exactly(10))
|
||||
->method('provideInitialState')
|
||||
->withConsecutive(
|
||||
[
|
||||
|
@ -71,6 +71,16 @@ class AdminSettingsTest extends TestCase {
|
|||
'allow_new_mail_accounts',
|
||||
$this->anything()
|
||||
],
|
||||
[
|
||||
Application::APP_ID,
|
||||
'enabled_thread_summary',
|
||||
$this->anything()
|
||||
],
|
||||
[
|
||||
Application::APP_ID,
|
||||
'enabled_llm_backend',
|
||||
$this->anything()
|
||||
],
|
||||
[
|
||||
Application::APP_ID,
|
||||
'google_oauth_client_id',
|
||||
|
|
Загрузка…
Ссылка в новой задаче