Skip to content

Commit 3fe48b2

Browse files
committed
feat: enhance conversation listing with pagination, sorting, and search capabilities
1 parent e3e2749 commit 3fe48b2

File tree

14 files changed

+333
-311
lines changed

14 files changed

+333
-311
lines changed

api/models/Conversation.js

Lines changed: 53 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -87,11 +87,13 @@ module.exports = {
8787
*/
8888
saveConvo: async (req, { conversationId, newConversationId, ...convo }, metadata) => {
8989
try {
90-
if (metadata && metadata?.context) {
90+
if (metadata?.context) {
9191
logger.debug(`[saveConvo] ${metadata.context}`);
9292
}
93+
9394
const messages = await getMessages({ conversationId }, '_id');
9495
const update = { ...convo, messages, user: req.user.id };
96+
9597
if (newConversationId) {
9698
update.conversationId = newConversationId;
9799
}
@@ -143,26 +145,44 @@ module.exports = {
143145
},
144146
getConvosByCursor: async (
145147
user,
146-
{ cursor, limit = 25, isArchived = false, tags, order = 'desc' } = {},
148+
{ cursor, limit = 25, isArchived = false, tags, search, order = 'desc' } = {},
147149
) => {
148-
const query = { user };
150+
const filters = [{ user }];
149151

150152
if (isArchived) {
151-
query.isArchived = true;
153+
filters.push({ isArchived: true });
152154
} else {
153-
query.$or = [{ isArchived: false }, { isArchived: { $exists: false } }];
155+
filters.push({ $or: [{ isArchived: false }, { isArchived: { $exists: false } }] });
154156
}
155157

156158
if (Array.isArray(tags) && tags.length > 0) {
157-
query.tags = { $in: tags };
159+
filters.push({ tags: { $in: tags } });
158160
}
159161

160-
query.$and = [{ $or: [{ expiredAt: null }, { expiredAt: { $exists: false } }] }];
162+
filters.push({ $or: [{ expiredAt: null }, { expiredAt: { $exists: false } }] });
163+
164+
if (search) {
165+
try {
166+
const meiliResults = await Conversation.meiliSearch(search);
167+
const matchingIds = Array.isArray(meiliResults.hits)
168+
? meiliResults.hits.map((result) => result.conversationId)
169+
: [];
170+
if (!matchingIds.length) {
171+
return { conversations: [], nextCursor: null };
172+
}
173+
filters.push({ conversationId: { $in: matchingIds } });
174+
} catch (error) {
175+
logger.error('[getConvosByCursor] Error during meiliSearch', error);
176+
return { message: 'Error during meiliSearch' };
177+
}
178+
}
161179

162180
if (cursor) {
163-
query.updatedAt = { $lt: new Date(cursor) };
181+
filters.push({ updatedAt: { $lt: new Date(cursor) } });
164182
}
165183

184+
const query = filters.length === 1 ? filters[0] : { $and: filters };
185+
166186
try {
167187
const convos = await Conversation.find(query)
168188
.select('conversationId endpoint title createdAt updatedAt user')
@@ -184,45 +204,34 @@ module.exports = {
184204
},
185205
getConvosQueried: async (user, convoIds, cursor = null, limit = 25) => {
186206
try {
187-
if (!convoIds || convoIds.length === 0) {
207+
if (!convoIds?.length) {
188208
return { conversations: [], nextCursor: null, convoMap: {} };
189209
}
190210

191-
const convoMap = {};
192-
const promises = [];
193-
194-
convoIds.forEach((convo) =>
195-
promises.push(
196-
Conversation.findOne({
197-
user,
198-
conversationId: convo.conversationId,
199-
$or: [{ expiredAt: { $exists: false } }, { expiredAt: null }],
200-
}).lean(),
201-
),
202-
);
211+
const conversationIds = convoIds.map((convo) => convo.conversationId);
203212

204-
// Fetch all matching conversations and filter out any falsy results
205-
const results = (await Promise.all(promises)).filter(Boolean);
213+
const results = await Conversation.find({
214+
user,
215+
conversationId: { $in: conversationIds },
216+
$or: [{ expiredAt: { $exists: false } }, { expiredAt: null }],
217+
}).lean();
206218

207-
// Sort conversations by updatedAt descending (most recent first)
208219
results.sort((a, b) => new Date(b.updatedAt) - new Date(a.updatedAt));
209220

210-
// If a cursor is provided and not "start", filter out recrods newer or equal to the cursor date
211221
let filtered = results;
212222
if (cursor && cursor !== 'start') {
213223
const cursorDate = new Date(cursor);
214224
filtered = results.filter((convo) => new Date(convo.updatedAt) < cursorDate);
215225
}
216226

217-
// Retrieve limit + 1 results to determine if there's a next page.
218227
const limited = filtered.slice(0, limit + 1);
219228
let nextCursor = null;
220229
if (limited.length > limit) {
221230
const lastConvo = limited.pop();
222231
nextCursor = lastConvo.updatedAt.toISOString();
223232
}
224233

225-
// Build convoMap for ease of access if required by caller
234+
const convoMap = {};
226235
limited.forEach((convo) => {
227236
convoMap[convo.conversationId] = convo;
228237
});
@@ -268,10 +277,22 @@ module.exports = {
268277
* logger.error(result); // { n: 5, ok: 1, deletedCount: 5, messages: { n: 10, ok: 1, deletedCount: 10 } }
269278
*/
270279
deleteConvos: async (user, filter) => {
271-
let toRemove = await Conversation.find({ ...filter, user }).select('conversationId');
272-
const ids = toRemove.map((instance) => instance.conversationId);
273-
let deleteCount = await Conversation.deleteMany({ ...filter, user });
274-
deleteCount.messages = await deleteMessages({ conversationId: { $in: ids } });
275-
return deleteCount;
280+
try {
281+
const userFilter = { ...filter, user };
282+
283+
const conversations = await Conversation.find(userFilter).select('conversationId');
284+
const conversationIds = conversations.map((c) => c.conversationId);
285+
286+
const deleteConvoResult = await Conversation.deleteMany(userFilter);
287+
288+
const deleteMessagesResult = await deleteMessages({
289+
conversationId: { $in: conversationIds },
290+
});
291+
292+
return { ...deleteConvoResult, messages: deleteMessagesResult };
293+
} catch (error) {
294+
logger.error('[deleteConvos] Error deleting conversations and messages', error);
295+
throw error;
296+
}
276297
},
277298
};

api/server/routes/convos.js

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,10 @@ const requireJwtAuth = require('~/server/middleware/requireJwtAuth');
88
const { importConversations } = require('~/server/utils/import');
99
const { createImportLimiters } = require('~/server/middleware');
1010
const { deleteToolCalls } = require('~/models/ToolCall');
11+
const { isEnabled, sleep } = require('~/server/utils');
1112
const getLogStores = require('~/cache/getLogStores');
12-
const { sleep } = require('~/server/utils');
1313
const { logger } = require('~/config');
14+
1415
const assistantClients = {
1516
[EModelEndpoint.azureAssistants]: require('~/server/services/Endpoints/azureAssistants'),
1617
[EModelEndpoint.assistants]: require('~/server/services/Endpoints/assistants'),
@@ -20,21 +21,26 @@ const router = express.Router();
2021
router.use(requireJwtAuth);
2122

2223
router.get('/', async (req, res) => {
23-
// Limiting pagination as cursor may be undefined if not provided
2424
const limit = parseInt(req.query.limit, 10) || 25;
2525
const cursor = req.query.cursor;
26-
const isArchived = req.query.isArchived === 'true';
26+
const isArchived = isEnabled(req.query.isArchived);
27+
const search = req.query.search;
28+
const order = req.query.order || 'desc';
2729

2830
let tags;
2931
if (req.query.tags) {
3032
tags = Array.isArray(req.query.tags) ? req.query.tags : [req.query.tags];
3133
}
3234

33-
// Support for ordering; expects "asc" or "desc", defaults to descending order.
34-
const order = req.query.order || 'desc';
35-
3635
try {
37-
const result = await getConvosByCursor(req.user.id, { cursor, limit, isArchived, tags, order });
36+
const result = await getConvosByCursor(req.user.id, {
37+
cursor,
38+
limit,
39+
isArchived,
40+
tags,
41+
search,
42+
order,
43+
});
3844
res.status(200).json(result);
3945
} catch (error) {
4046
res.status(500).json({ error: 'Error fetching conversations' });

client/src/components/Nav/Nav.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ const Nav = memo(
7878

7979
const { data, fetchNextPage, isFetchingNextPage, refetch } = useConversationsInfiniteQuery(
8080
{
81-
cursor: null,
81+
pageSize: 25,
8282
isArchived: false,
8383
tags: tags.length === 0 ? undefined : tags,
8484
},

client/src/components/Nav/SettingsTabs/Data/SharedLinks.tsx

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,18 +9,18 @@ import {
99
OGDialogContent,
1010
OGDialogHeader,
1111
OGDialogTitle,
12-
Button,
1312
TooltipAnchor,
13+
Button,
1414
Label,
15-
} from '~/components/ui';
15+
Spinner,
16+
} from '~/components';
1617
import { useDeleteSharedLinkMutation, useSharedLinksQuery } from '~/data-provider';
1718
import OGDialogTemplate from '~/components/ui/OGDialogTemplate';
1819
import { useLocalize, useMediaQuery } from '~/hooks';
1920
import DataTable from '~/components/ui/DataTable';
2021
import { NotificationSeverity } from '~/common';
2122
import { useToastContext } from '~/Providers';
2223
import { formatDate } from '~/utils';
23-
import { Spinner } from '~/components/svg';
2424

2525
const PAGE_SIZE = 25;
2626

@@ -37,6 +37,7 @@ export default function SharedLinks() {
3737
const { showToast } = useToastContext();
3838
const isSmallScreen = useMediaQuery('(max-width: 768px)');
3939
const [queryParams, setQueryParams] = useState<SharedLinksListParams>(DEFAULT_PARAMS);
40+
const [deleteRow, setDeleteRow] = useState<SharedLinkItem | null>(null);
4041
const [isDeleteOpen, setIsDeleteOpen] = useState(false);
4142
const [isOpen, setIsOpen] = useState(false);
4243

@@ -144,8 +145,6 @@ export default function SharedLinks() {
144145
await fetchNextPage();
145146
}, [fetchNextPage, hasNextPage, isFetchingNextPage]);
146147

147-
const [deleteRow, setDeleteRow] = useState<SharedLinkItem | null>(null);
148-
149148
const confirmDelete = useCallback(() => {
150149
if (deleteRow) {
151150
handleDelete([deleteRow]);
@@ -293,6 +292,7 @@ export default function SharedLinks() {
293292
showCheckboxes={false}
294293
onFilterChange={debouncedFilterChange}
295294
filterValue={queryParams.search}
295+
isLoading={isLoading}
296296
/>
297297
</OGDialogContent>
298298
</OGDialog>

client/src/components/Nav/SettingsTabs/General/ArchivedChats.tsx

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,17 @@
1-
import { useLocalize } from '~/hooks';
2-
import OGDialogTemplate from '~/components/ui/OGDialogTemplate';
1+
import { useState } from 'react';
32
import { OGDialog, OGDialogTrigger, Button } from '~/components';
3+
import OGDialogTemplate from '~/components/ui/OGDialogTemplate';
44
import ArchivedChatsTable from './ArchivedChatsTable';
5+
import { useLocalize } from '~/hooks';
56

67
export default function ArchivedChats() {
78
const localize = useLocalize();
9+
const [isOpen, setIsOpen] = useState(false);
810

911
return (
1012
<div className="flex items-center justify-between">
1113
<div>{localize('com_nav_archived_chats')}</div>
12-
<OGDialog>
14+
<OGDialog open={isOpen} onOpenChange={setIsOpen}>
1315
<OGDialogTrigger asChild>
1416
<Button variant="outline" aria-label="Archived chats">
1517
{localize('com_nav_archived_chats_manage')}
@@ -19,7 +21,7 @@ export default function ArchivedChats() {
1921
title={localize('com_nav_archived_chats')}
2022
className="max-w-[1000px]"
2123
showCancelButton={false}
22-
main={<ArchivedChatsTable />}
24+
main={<ArchivedChatsTable isOpen={isOpen} onOpenChange={setIsOpen} />}
2325
/>
2426
</OGDialog>
2527
</div>

0 commit comments

Comments
 (0)