diff --git a/appinfo/routes.php b/appinfo/routes.php index 1022f74c..142b7a73 100644 --- a/appinfo/routes.php +++ b/appinfo/routes.php @@ -44,6 +44,7 @@ ['name' => 'chattyLLM#updateSessionTitle', 'url' => '/chat/update_session', 'verb' => 'PATCH'], ['name' => 'chattyLLM#deleteSession', 'url' => '/chat/delete_session', 'verb' => 'DELETE'], ['name' => 'chattyLLM#getSessions', 'url' => '/chat/sessions', 'verb' => 'GET'], + ['name' => 'chattyLLM#searchChat', 'url' => '/chat/search', 'verb' => 'GET'], ['name' => 'chattyLLM#newMessage', 'url' => '/chat/new_message', 'verb' => 'PUT'], ['name' => 'chattyLLM#deleteMessage', 'url' => '/chat/delete_message', 'verb' => 'DELETE'], ['name' => 'chattyLLM#getMessages', 'url' => '/chat/messages', 'verb' => 'GET'], diff --git a/lib/Controller/ChattyLLMController.php b/lib/Controller/ChattyLLMController.php index e996cb0e..59a516cb 100644 --- a/lib/Controller/ChattyLLMController.php +++ b/lib/Controller/ChattyLLMController.php @@ -311,6 +311,39 @@ public function getSessions(): JSONResponse { } } + /** + * Search chat messages by content + * + * Returns sessions that contain matching messages. + * + * @param string $query Search query (min 2 characters) + * @return JSONResponse}, array{}>|JSONResponse + * + * 200: Search results + * 401: Not logged in + * 500: Failed to search + */ + #[NoAdminRequired] + #[OpenAPI(scope: OpenAPI::SCOPE_DEFAULT, tags: ['chat_api'])] + public function searchChat(string $query = ''): JSONResponse { + if ($this->userId === null) { + return new JSONResponse(['error' => $this->l10n->t('User not logged in')], Http::STATUS_UNAUTHORIZED); + } + + $query = trim($query); + if (empty($query) || strlen($query) < 2) { + return new JSONResponse(['sessions' => []]); + } + + try { + $sessions = $this->sessionMapper->searchSessionsByMessageContent($this->userId, $query); + return new JSONResponse(['sessions' => $sessions]); + } catch (\OCP\DB\Exception $e) { + $this->logger->warning('Failed to search chat messages', ['exception' => $e]); + return new JSONResponse(['error' => $this->l10n->t('Failed to search chat sessions')], Http::STATUS_INTERNAL_SERVER_ERROR); + } + } + /** * Add a message * diff --git a/lib/Db/ChattyLLM/SessionMapper.php b/lib/Db/ChattyLLM/SessionMapper.php index 9006bcf6..4a167eab 100644 --- a/lib/Db/ChattyLLM/SessionMapper.php +++ b/lib/Db/ChattyLLM/SessionMapper.php @@ -185,4 +185,24 @@ public function updateSessionIsRemembered(?string $userId, int $sessionId, bool $session->setIsRemembered($is_remembered); $this->update($session); } + + /** + * @return array + * @throws \OCP\DB\Exception + */ + public function searchSessionsByMessageContent(string $userId, string $query): array { + $qb = $this->db->getQueryBuilder(); + $qb->select('s.id', 's.user_id', 's.title', 's.timestamp', 's.agency_conversation_token', 's.agency_pending_actions', 's.summary', 's.is_summary_up_to_date', 's.is_remembered') + ->from($this->getTableName(), 's') + ->innerJoin('s', 'assistant_chat_msgs', 'm', $qb->expr()->eq('s.id', 'm.session_id')) + ->where($qb->expr()->eq('s.user_id', $qb->createPositionalParameter($userId, IQueryBuilder::PARAM_STR))) + ->andWhere($qb->expr()->in('m.role', $qb->createParameter('roles'))) + ->andWhere($qb->expr()->iLike('m.content', $qb->createPositionalParameter('%' . $query . '%', IQueryBuilder::PARAM_STR))) + ->groupBy('s.id') + ->orderBy('s.timestamp', 'DESC'); + + $qb->setParameter('roles', ['human', 'assistant'], IQueryBuilder::PARAM_STR_ARRAY); + + return $this->findEntities($qb); + } } diff --git a/src/components/ChattyLLM/ChattyLLMInputForm.vue b/src/components/ChattyLLM/ChattyLLMInputForm.vue index b6164487..30d9200f 100644 --- a/src/components/ChattyLLM/ChattyLLMInputForm.vue +++ b/src/components/ChattyLLM/ChattyLLMInputForm.vue @@ -6,6 +6,17 @@
+ @@ -17,11 +28,11 @@ {{ t('assistant', 'Loading conversations…') }}
-
- {{ t('assistant', 'No conversations yet') }} +
+ {{ searchQuery ? t('assistant', 'No matching conversations') : t('assistant', 'No conversations yet') }}
-
+
@@ -120,8 +131,12 @@ + @delete="deleteMessage" + @highlight-end="highlightedMessageIndex = null" />
+
+ {{ t('assistant', 'Search matches') }} + {{ searchMatchCurrentDisplay }}/{{ matchingMessageIndices.length }} +
+ + + + + + +
+

{{ t('assistant', 'Output shown here is generated by AI. Make sure to always double-check.') }}

@@ -186,6 +225,8 @@