diff --git a/appinfo/routes.php b/appinfo/routes.php index e06389476..3dc4df1d0 100644 --- a/appinfo/routes.php +++ b/appinfo/routes.php @@ -45,10 +45,12 @@ ['name' => 'chattyLLM#newSession', 'url' => '/chat/sessions', 'verb' => 'POST', 'postfix' => 'restful'], ['name' => 'chattyLLM#updateChatSession', 'url' => '/chat/sessions/{sessionId}', 'verb' => 'PUT', 'postfix' => 'restful'], ['name' => 'chattyLLM#deleteSession', 'url' => '/chat/sessions/{sessionId}', 'verb' => 'DELETE', 'postfix' => 'restful'], + ['name' => 'chattyLLM#deleteSessions', 'url' => '/chat/sessions', 'verb' => 'DELETE', 'postfix' => 'restful'], ['name' => 'chattyLLM#newSession', 'url' => '/chat/new_session', 'verb' => 'PUT'], ['name' => 'chattyLLM#updateSessionTitle', 'url' => '/chat/update_session', 'verb' => 'PATCH'], ['name' => 'chattyLLM#deleteSession', 'url' => '/chat/delete_session', 'verb' => 'DELETE'], + ['name' => 'chattyLLM#deleteSessions', 'url' => '/chat/delete_sessions', 'verb' => 'DELETE'], ['name' => 'chattyLLM#getSessions', 'url' => '/chat/sessions', 'verb' => 'GET'], ['name' => 'chattyLLM#newMessage', 'url' => '/chat/new_message', 'verb' => 'PUT'], ['name' => 'chattyLLM#deleteMessage', 'url' => '/chat/delete_message', 'verb' => 'DELETE'], diff --git a/lib/Controller/ChattyLLMController.php b/lib/Controller/ChattyLLMController.php index 0772d325c..cc750a605 100644 --- a/lib/Controller/ChattyLLMController.php +++ b/lib/Controller/ChattyLLMController.php @@ -306,6 +306,36 @@ public function deleteSession(int $sessionId): JSONResponse { } } + /** + * Delete chat sessions + * + * Delete several chat sessions by ID + * + * @param list $sessionIds The session IDs + * @return JSONResponse|JSONResponse + * + * 200: The sessions have been deleted successfully + * 400: The list of session IDs is empty or invalid + * 401: Not logged in + */ + #[NoAdminRequired] + #[OpenAPI(scope: OpenAPI::SCOPE_DEFAULT, tags: ['chat_api'])] + public function deleteSessions(array $sessionIds): JSONResponse { + if ($sessionIds === []) { + return new JSONResponse(['error' => $this->l10n->t('Invalid session IDs')], Http::STATUS_BAD_REQUEST); + } + try { + // we don't delete the tasks + $this->chatService->deleteSessions($this->userId, $sessionIds); + return new JSONResponse(); + } catch (InternalException $e) { + $this->logger->warning('Failed to delete the chat sessions', ['exception' => $e]); + return new JSONResponse(['error' => $this->l10n->t('Failed to delete the chat sessions')], Http::STATUS_INTERNAL_SERVER_ERROR); + } catch (\OCA\Assistant\Service\UnauthorizedException $e) { + return new JSONResponse(['error' => $this->l10n->t('User not logged in')], Http::STATUS_UNAUTHORIZED); + } + } + /** * Get chat sessions * diff --git a/lib/Db/ChattyLLM/MessageMapper.php b/lib/Db/ChattyLLM/MessageMapper.php index 86204fc27..8b44208e5 100644 --- a/lib/Db/ChattyLLM/MessageMapper.php +++ b/lib/Db/ChattyLLM/MessageMapper.php @@ -177,6 +177,23 @@ public function deleteMessagesBySession(int $sessionId): void { $qb->executeStatement(); } + /** + * @param list $sessionIds + * @throws \OCP\DB\Exception + * @throws \RuntimeException + * @return void + */ + public function deleteMessagesBySessions(array $sessionIds): void { + $qb = $this->db->getQueryBuilder(); + $qb->delete($this->getTableName()) + ->where($qb->expr()->in('session_id', $qb->createParameter('ids'))); + + foreach (array_chunk($sessionIds, IQueryBuilder::MAX_IN_PARAMETERS) as $chunk) { + $qb->setParameter('ids', $chunk, IQueryBuilder::PARAM_INT_ARRAY); + $qb->executeStatement(); + } + } + /** * @param int $sessionId * @param integer $messageId diff --git a/lib/Db/ChattyLLM/SessionMapper.php b/lib/Db/ChattyLLM/SessionMapper.php index c8359173a..5c089c9bd 100644 --- a/lib/Db/ChattyLLM/SessionMapper.php +++ b/lib/Db/ChattyLLM/SessionMapper.php @@ -218,6 +218,43 @@ public function deleteSession(string $userId, int $sessionId) { $qb->executeStatement(); } + /** + * @param string $userId + * @param list $sessionIds + * @return list + * @throws \OCP\DB\Exception + */ + public function getUserSessionsByIds(string $userId, array $sessionIds): array { + $sessions = []; + foreach (array_chunk($sessionIds, IQueryBuilder::MAX_IN_PARAMETERS) as $chunk) { + $qb = $this->db->getQueryBuilder(); + $qb->select(Session::$columns) + ->from($this->getTableName()) + ->where($qb->expr()->eq('user_id', $qb->createPositionalParameter($userId, IQueryBuilder::PARAM_STR))) + ->andWhere($qb->expr()->in('id', $qb->createPositionalParameter($chunk, IQueryBuilder::PARAM_INT_ARRAY))); + $sessions = array_merge($sessions, $this->findEntities($qb)); + } + return $sessions; + } + + /** + * @param string $userId + * @param list $sessionIds + * @throws \OCP\DB\Exception + * @throws \RuntimeException + */ + public function deleteSessionsByUser(string $userId, array $sessionIds): void { + $qb = $this->db->getQueryBuilder(); + $qb->delete($this->getTableName()) + ->where($qb->expr()->eq('user_id', $qb->createNamedParameter($userId, IQueryBuilder::PARAM_STR))) + ->andWhere($qb->expr()->in('id', $qb->createParameter('ids'))); + + foreach (array_chunk($sessionIds, IQueryBuilder::MAX_IN_PARAMETERS) as $chunk) { + $qb->setParameter('ids', $chunk, IQueryBuilder::PARAM_INT_ARRAY); + $qb->executeStatement(); + } + } + /** * @throws \OCP\DB\Exception */ diff --git a/lib/Service/ChatService.php b/lib/Service/ChatService.php index 18adfc6c3..1f27de531 100644 --- a/lib/Service/ChatService.php +++ b/lib/Service/ChatService.php @@ -18,6 +18,7 @@ use OCP\DB\Exception; use OCP\Exceptions\AppConfigTypeConflictException; use OCP\IAppConfig; +use OCP\IDBConnection; use OCP\IL10N; use OCP\IUserManager; use OCP\TaskProcessing\Exception\PreConditionNotMetException; @@ -36,6 +37,7 @@ public function __construct( private readonly IL10N $l10n, private readonly SessionMapper $sessionMapper, private readonly MessageMapper $messageMapper, + private readonly IDBConnection $db, private readonly SessionSummaryService $sessionSummaryService, private readonly IManager $taskProcessingManager, private readonly LoggerInterface $logger, @@ -152,6 +154,47 @@ public function deleteSession(?string $userId, int $sessionId): void { } } + /** + * @param string|null $userId + * @param list $sessionIds + * @throws InternalException + * @throws UnauthorizedException + */ + public function deleteSessions(?string $userId, array $sessionIds): void { + if ($userId === null) { + throw new UnauthorizedException($this->l10n->t('Unauthorized')); + } + + $sessionIds = array_values(array_unique(array_map(static function ($sessionId) { + return (int)$sessionId; + }, $sessionIds))); + + if ($sessionIds === []) { + return; + } + + try { + $ownedSessions = $this->sessionMapper->getUserSessionsByIds($userId, $sessionIds); + $ownedSessionIds = array_map(static function (Session $session) { + return $session->getId(); + }, $ownedSessions); + + if ($ownedSessionIds === []) { + return; + } + + $this->db->beginTransaction(); + $this->sessionMapper->deleteSessionsByUser($userId, $ownedSessionIds); + $this->messageMapper->deleteMessagesBySessions($ownedSessionIds); + $this->db->commit(); + } catch (Exception|\RuntimeException $e) { + if ($this->db->inTransaction()) { + $this->db->rollBack(); + } + throw new InternalException(previous: $e); + } + } + /** * @throws InternalException */ diff --git a/openapi.json b/openapi.json index ca07990e5..ec9569407 100644 --- a/openapi.json +++ b/openapi.json @@ -3818,6 +3818,138 @@ } } }, + "delete": { + "operationId": "chattyllm-delete-sessions-restful", + "summary": "Delete chat sessions", + "description": "Delete several chat sessions by ID", + "tags": [ + "chat_api" + ], + "security": [ + { + "bearer_auth": [] + }, + { + "basic_auth": [] + } + ], + "parameters": [ + { + "name": "sessionIds[]", + "in": "query", + "description": "The session IDs", + "required": true, + "schema": { + "type": "array", + "items": { + "type": "integer", + "format": "int64" + } + } + }, + { + "name": "OCS-APIRequest", + "in": "header", + "description": "Required to be true for the API request to pass", + "required": true, + "schema": { + "type": "boolean", + "default": true + } + } + ], + "responses": { + "200": { + "description": "The sessions have been deleted successfully", + "content": { + "application/json": { + "schema": { + "type": "object" + } + } + } + }, + "500": { + "description": "", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "error" + ], + "properties": { + "error": { + "type": "string" + } + } + } + } + } + }, + "401": { + "description": "Not logged in", + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "type": "object", + "required": [ + "error" + ], + "properties": { + "error": { + "type": "string" + } + } + }, + { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + ] + } + } + } + }, + "400": { + "description": "The list of session IDs is empty or invalid", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "error" + ], + "properties": { + "error": { + "type": "string" + } + } + } + } + } + } + } + }, "get": { "operationId": "chattyllm-get-sessions", "summary": "Get chat sessions", @@ -4598,6 +4730,140 @@ } } }, + "/ocs/v2.php/apps/assistant/chat/delete_sessions": { + "delete": { + "operationId": "chattyllm-delete-sessions", + "summary": "Delete chat sessions", + "description": "Delete several chat sessions by ID", + "tags": [ + "chat_api" + ], + "security": [ + { + "bearer_auth": [] + }, + { + "basic_auth": [] + } + ], + "parameters": [ + { + "name": "sessionIds[]", + "in": "query", + "description": "The session IDs", + "required": true, + "schema": { + "type": "array", + "items": { + "type": "integer", + "format": "int64" + } + } + }, + { + "name": "OCS-APIRequest", + "in": "header", + "description": "Required to be true for the API request to pass", + "required": true, + "schema": { + "type": "boolean", + "default": true + } + } + ], + "responses": { + "200": { + "description": "The sessions have been deleted successfully", + "content": { + "application/json": { + "schema": { + "type": "object" + } + } + } + }, + "500": { + "description": "", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "error" + ], + "properties": { + "error": { + "type": "string" + } + } + } + } + } + }, + "401": { + "description": "Not logged in", + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "type": "object", + "required": [ + "error" + ], + "properties": { + "error": { + "type": "string" + } + } + }, + { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + ] + } + } + } + }, + "400": { + "description": "The list of session IDs is empty or invalid", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "error" + ], + "properties": { + "error": { + "type": "string" + } + } + } + } + } + } + } + } + }, "/ocs/v2.php/apps/assistant/chat/new_message": { "put": { "operationId": "chattyllm-new-message", diff --git a/src/components/ChattyLLM/ChattyLLMInputForm.vue b/src/components/ChattyLLM/ChattyLLMInputForm.vue index 51da553de..401345e99 100644 --- a/src/components/ChattyLLM/ChattyLLMInputForm.vue +++ b/src/components/ChattyLLM/ChattyLLMInputForm.vue @@ -6,7 +6,7 @@
- @@ -14,6 +14,40 @@ + + + {{ t('assistant', 'Delete multiple conversations') }} + +
+
+ + {{ selectAllLabel }} + + + {{ t('assistant', 'Cancel') }} + +
+ + + {{ deleteSelectedLabel }} + +
{{ isAssignment ? t('assistant', 'Loading scheduled tasks…') : t('assistant', 'Loading conversations…') }} @@ -25,17 +59,17 @@ v-for="session in sessions" v-else :key="'conversation' + session.id" - :active="session.id === active?.id" + :active="deletionMode ? selectedSessionIds.includes(session.id) : session.id === active?.id" :name="getSessionTitle(session)" :title="getSessionTitle(session)" :aria-description="getSessionTitle(session)" :editable="false" :inline-actions="1" - @click="onSessionSelect(session)"> + @click="deletionMode ? toggleSessionSelection(session.id) : onSessionSelect(session)">