Skip to content

Commit

Permalink
feat: enhance conversation listing with pagination, sorting, and sear…
Browse files Browse the repository at this point in the history
…ch capabilities
  • Loading branch information
berry-13 committed Feb 16, 2025
1 parent e3e2749 commit 3fe48b2
Show file tree
Hide file tree
Showing 14 changed files with 333 additions and 311 deletions.
85 changes: 53 additions & 32 deletions api/models/Conversation.js
Original file line number Diff line number Diff line change
Expand Up @@ -87,11 +87,13 @@ module.exports = {
*/
saveConvo: async (req, { conversationId, newConversationId, ...convo }, metadata) => {
try {
if (metadata && metadata?.context) {
if (metadata?.context) {
logger.debug(`[saveConvo] ${metadata.context}`);
}

const messages = await getMessages({ conversationId }, '_id');
const update = { ...convo, messages, user: req.user.id };

if (newConversationId) {
update.conversationId = newConversationId;
}
Expand Down Expand Up @@ -143,26 +145,44 @@ module.exports = {
},
getConvosByCursor: async (
user,
{ cursor, limit = 25, isArchived = false, tags, order = 'desc' } = {},
{ cursor, limit = 25, isArchived = false, tags, search, order = 'desc' } = {},
) => {
const query = { user };
const filters = [{ user }];

if (isArchived) {
query.isArchived = true;
filters.push({ isArchived: true });
} else {
query.$or = [{ isArchived: false }, { isArchived: { $exists: false } }];
filters.push({ $or: [{ isArchived: false }, { isArchived: { $exists: false } }] });
}

if (Array.isArray(tags) && tags.length > 0) {
query.tags = { $in: tags };
filters.push({ tags: { $in: tags } });
}

query.$and = [{ $or: [{ expiredAt: null }, { expiredAt: { $exists: false } }] }];
filters.push({ $or: [{ expiredAt: null }, { expiredAt: { $exists: false } }] });

if (search) {
try {
const meiliResults = await Conversation.meiliSearch(search);
const matchingIds = Array.isArray(meiliResults.hits)
? meiliResults.hits.map((result) => result.conversationId)
: [];
if (!matchingIds.length) {
return { conversations: [], nextCursor: null };
}
filters.push({ conversationId: { $in: matchingIds } });
} catch (error) {
logger.error('[getConvosByCursor] Error during meiliSearch', error);
return { message: 'Error during meiliSearch' };
}
}

if (cursor) {
query.updatedAt = { $lt: new Date(cursor) };
filters.push({ updatedAt: { $lt: new Date(cursor) } });
}

const query = filters.length === 1 ? filters[0] : { $and: filters };

try {
const convos = await Conversation.find(query)
.select('conversationId endpoint title createdAt updatedAt user')
Expand All @@ -184,45 +204,34 @@ module.exports = {
},
getConvosQueried: async (user, convoIds, cursor = null, limit = 25) => {
try {
if (!convoIds || convoIds.length === 0) {
if (!convoIds?.length) {
return { conversations: [], nextCursor: null, convoMap: {} };
}

const convoMap = {};
const promises = [];

convoIds.forEach((convo) =>
promises.push(
Conversation.findOne({
user,
conversationId: convo.conversationId,
$or: [{ expiredAt: { $exists: false } }, { expiredAt: null }],
}).lean(),
),
);
const conversationIds = convoIds.map((convo) => convo.conversationId);

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

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

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

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

// Build convoMap for ease of access if required by caller
const convoMap = {};
limited.forEach((convo) => {
convoMap[convo.conversationId] = convo;
});
Expand Down Expand Up @@ -268,10 +277,22 @@ module.exports = {
* logger.error(result); // { n: 5, ok: 1, deletedCount: 5, messages: { n: 10, ok: 1, deletedCount: 10 } }
*/
deleteConvos: async (user, filter) => {
let toRemove = await Conversation.find({ ...filter, user }).select('conversationId');
const ids = toRemove.map((instance) => instance.conversationId);
let deleteCount = await Conversation.deleteMany({ ...filter, user });
deleteCount.messages = await deleteMessages({ conversationId: { $in: ids } });
return deleteCount;
try {
const userFilter = { ...filter, user };

const conversations = await Conversation.find(userFilter).select('conversationId');
const conversationIds = conversations.map((c) => c.conversationId);

const deleteConvoResult = await Conversation.deleteMany(userFilter);

const deleteMessagesResult = await deleteMessages({
conversationId: { $in: conversationIds },
});

return { ...deleteConvoResult, messages: deleteMessagesResult };
} catch (error) {
logger.error('[deleteConvos] Error deleting conversations and messages', error);
throw error;
}
},
};
20 changes: 13 additions & 7 deletions api/server/routes/convos.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,10 @@ const requireJwtAuth = require('~/server/middleware/requireJwtAuth');
const { importConversations } = require('~/server/utils/import');
const { createImportLimiters } = require('~/server/middleware');
const { deleteToolCalls } = require('~/models/ToolCall');
const { isEnabled, sleep } = require('~/server/utils');
const getLogStores = require('~/cache/getLogStores');
const { sleep } = require('~/server/utils');
const { logger } = require('~/config');

const assistantClients = {
[EModelEndpoint.azureAssistants]: require('~/server/services/Endpoints/azureAssistants'),
[EModelEndpoint.assistants]: require('~/server/services/Endpoints/assistants'),
Expand All @@ -20,21 +21,26 @@ const router = express.Router();
router.use(requireJwtAuth);

router.get('/', async (req, res) => {
// Limiting pagination as cursor may be undefined if not provided
const limit = parseInt(req.query.limit, 10) || 25;
const cursor = req.query.cursor;
const isArchived = req.query.isArchived === 'true';
const isArchived = isEnabled(req.query.isArchived);
const search = req.query.search;
const order = req.query.order || 'desc';

let tags;
if (req.query.tags) {
tags = Array.isArray(req.query.tags) ? req.query.tags : [req.query.tags];
}

// Support for ordering; expects "asc" or "desc", defaults to descending order.
const order = req.query.order || 'desc';

try {
const result = await getConvosByCursor(req.user.id, { cursor, limit, isArchived, tags, order });
const result = await getConvosByCursor(req.user.id, {
cursor,
limit,
isArchived,
tags,
search,
order,
});
res.status(200).json(result);
} catch (error) {
res.status(500).json({ error: 'Error fetching conversations' });
Expand Down
2 changes: 1 addition & 1 deletion client/src/components/Nav/Nav.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ const Nav = memo(

const { data, fetchNextPage, isFetchingNextPage, refetch } = useConversationsInfiniteQuery(
{
cursor: null,
pageSize: 25,
isArchived: false,
tags: tags.length === 0 ? undefined : tags,
},
Expand Down
10 changes: 5 additions & 5 deletions client/src/components/Nav/SettingsTabs/Data/SharedLinks.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,18 +9,18 @@ import {
OGDialogContent,
OGDialogHeader,
OGDialogTitle,
Button,
TooltipAnchor,
Button,
Label,
} from '~/components/ui';
Spinner,
} from '~/components';
import { useDeleteSharedLinkMutation, useSharedLinksQuery } from '~/data-provider';
import OGDialogTemplate from '~/components/ui/OGDialogTemplate';
import { useLocalize, useMediaQuery } from '~/hooks';
import DataTable from '~/components/ui/DataTable';
import { NotificationSeverity } from '~/common';
import { useToastContext } from '~/Providers';
import { formatDate } from '~/utils';
import { Spinner } from '~/components/svg';

const PAGE_SIZE = 25;

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

Expand Down Expand Up @@ -144,8 +145,6 @@ export default function SharedLinks() {
await fetchNextPage();
}, [fetchNextPage, hasNextPage, isFetchingNextPage]);

const [deleteRow, setDeleteRow] = useState<SharedLinkItem | null>(null);

const confirmDelete = useCallback(() => {
if (deleteRow) {
handleDelete([deleteRow]);
Expand Down Expand Up @@ -293,6 +292,7 @@ export default function SharedLinks() {
showCheckboxes={false}
onFilterChange={debouncedFilterChange}
filterValue={queryParams.search}
isLoading={isLoading}
/>
</OGDialogContent>
</OGDialog>
Expand Down
10 changes: 6 additions & 4 deletions client/src/components/Nav/SettingsTabs/General/ArchivedChats.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,17 @@
import { useLocalize } from '~/hooks';
import OGDialogTemplate from '~/components/ui/OGDialogTemplate';
import { useState } from 'react';
import { OGDialog, OGDialogTrigger, Button } from '~/components';
import OGDialogTemplate from '~/components/ui/OGDialogTemplate';
import ArchivedChatsTable from './ArchivedChatsTable';
import { useLocalize } from '~/hooks';

export default function ArchivedChats() {
const localize = useLocalize();
const [isOpen, setIsOpen] = useState(false);

return (
<div className="flex items-center justify-between">
<div>{localize('com_nav_archived_chats')}</div>
<OGDialog>
<OGDialog open={isOpen} onOpenChange={setIsOpen}>
<OGDialogTrigger asChild>
<Button variant="outline" aria-label="Archived chats">
{localize('com_nav_archived_chats_manage')}
Expand All @@ -19,7 +21,7 @@ export default function ArchivedChats() {
title={localize('com_nav_archived_chats')}
className="max-w-[1000px]"
showCancelButton={false}
main={<ArchivedChatsTable />}
main={<ArchivedChatsTable isOpen={isOpen} onOpenChange={setIsOpen} />}
/>
</OGDialog>
</div>
Expand Down
Loading

0 comments on commit 3fe48b2

Please sign in to comment.