Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add polling for pending notifications #1152

Merged
merged 3 commits into from
Sep 12, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions backend/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
from routes.crawl_routes import crawl_router
from routes.explore_routes import explore_router
from routes.misc_routes import misc_router
from routes.notification_routes import notification_router
from routes.prompt_routes import prompt_router
from routes.subscription_routes import subscription_router
from routes.upload_routes import upload_router
Expand Down Expand Up @@ -54,6 +55,7 @@ async def startup_event():
app.include_router(api_key_router)
app.include_router(subscription_router)
app.include_router(prompt_router)
app.include_router(notification_router)


@app.exception_handler(HTTPException)
Expand Down
24 changes: 24 additions & 0 deletions backend/routes/notification_routes.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
from uuid import UUID

from auth import AuthBearer
from fastapi import APIRouter, Depends
from repository.notification.get_chat_notifications import (
get_chat_notifications,
)

notification_router = APIRouter()


@notification_router.get(
"/notifications/{chat_id}",
dependencies=[Depends(AuthBearer())],
tags=["Notification"],
)
async def get_notifications(
chat_id: UUID,
):
"""
Get notifications by chat_id
"""

return get_chat_notifications(chat_id)
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,12 @@ export const ActionsBar = (): JSX.Element => {
shouldDisplayUploadCard,
setShouldDisplayUploadCard,
hasPendingRequests,
setHasPendingRequests,
} = useActionBar();
const { addContent, contents, feedBrain, removeContent } =
useKnowledgeUploader();
useKnowledgeUploader({
setHasPendingRequests,
});

const { t } = useTranslation(["chat"]);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,5 +19,6 @@ export const useActionBar = () => {
shouldDisplayUploadCard,
setShouldDisplayUploadCard,
hasPendingRequests,
setHasPendingRequests,
};
};
Original file line number Diff line number Diff line change
@@ -1,38 +1,52 @@
/* eslint-disable max-lines */
import axios from "axios";
import { UUID } from "crypto";
import { useParams } from "next/navigation";
import { useParams, useRouter } from "next/navigation";
import { useCallback, useState } from "react";
import { useTranslation } from "react-i18next";

import { useChatApi } from "@/lib/api/chat/useChatApi";
import { useCrawlApi } from "@/lib/api/crawl/useCrawlApi";
import { useNotificationApi } from "@/lib/api/notification/useNotificationApi";
import { useUploadApi } from "@/lib/api/upload/useUploadApi";
import { useChatContext } from "@/lib/context";
import { useBrainContext } from "@/lib/context/BrainProvider/hooks/useBrainContext";
import { useToast } from "@/lib/hooks";

import { FeedItemCrawlType, FeedItemType, FeedItemUploadType } from "../types";

type UseKnowledgeUploaderProps = {
setHasPendingRequests: (hasPendingRequests: boolean) => void;
};
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
export const useKnowledgeUploader = () => {
export const useKnowledgeUploader = ({
setHasPendingRequests,
}: UseKnowledgeUploaderProps) => {
const [contents, setContents] = useState<FeedItemType[]>([]);
const { publish } = useToast();
const { uploadFile } = useUploadApi();
const { t } = useTranslation(["upload"]);
const { crawlWebsiteUrl } = useCrawlApi();
const { createChat } = useChatApi();

const { currentBrainId } = useBrainContext();
const { setNotifications } = useChatContext();
const { getChatNotifications } = useNotificationApi();
const router = useRouter();
const params = useParams();
const chatId = params?.chatId as UUID | undefined;

const { currentBrainId } = useBrainContext();
const addContent = (content: FeedItemType) => {
setContents((prevContents) => [...prevContents, content]);
};
const removeContent = (index: number) => {
setContents((prevContents) => prevContents.filter((_, i) => i !== index));
};

const fetchNotifications = async (currentChatId: UUID): Promise<void> => {
const fetchedNotifications = await getChatNotifications(currentChatId);
setNotifications(fetchedNotifications);
};

const crawlWebsiteHandler = useCallback(
async (url: string, brainId: UUID, chat_id: UUID) => {
// Configure parameters
Expand All @@ -50,6 +64,7 @@ export const useKnowledgeUploader = () => {
config,
chat_id,
});
await fetchNotifications(chat_id);
} catch (error: unknown) {
publish({
variant: "danger",
Expand Down Expand Up @@ -114,6 +129,7 @@ export const useKnowledgeUploader = () => {
}

try {
setHasPendingRequests(true);
const currentChatId = chatId ?? (await createChat("New Chat")).chat_id;
const uploadPromises = files.map((file) =>
uploadFileHandler(file, currentBrainId, currentChatId)
Expand All @@ -126,6 +142,12 @@ export const useKnowledgeUploader = () => {

setContents([]);

if (chatId === undefined) {
void router.push(`/chat/${currentChatId}`);
} else {
await fetchNotifications(currentChatId);
}

publish({
variant: "success",
text: t("knowledgeUploaded"),
Expand All @@ -135,6 +157,8 @@ export const useKnowledgeUploader = () => {
variant: "danger",
text: JSON.stringify(e),
});
} finally {
setHasPendingRequests(false);
}
};

Expand Down
45 changes: 42 additions & 3 deletions frontend/app/chat/[chatId]/hooks/useSelectedChatPage.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,59 @@
import { useQuery } from "@tanstack/react-query";
import { useParams } from "next/navigation";
import { useEffect } from "react";

import { useChatApi } from "@/lib/api/chat/useChatApi";
import { useNotificationApi } from "@/lib/api/notification/useNotificationApi";
import { useChatContext } from "@/lib/context";

import { getChatNotificationsQueryKey } from "../utils/getChatNotificationsQueryKey";
import { getMessagesFromChatItems } from "../utils/getMessagesFromChatItems";
import { getNotificationsFromChatItems } from "../utils/getNotificationsFromChatItems";

// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
export const useSelectedChatPage = () => {
const { setMessages, setNotifications } = useChatContext();
const { setMessages, setNotifications, notifications } = useChatContext();
const { getChatItems } = useChatApi();

const { getChatNotifications } = useNotificationApi();
const params = useParams();

const chatId = params?.chatId as string | undefined;

const chatNotificationsQueryKey = getChatNotificationsQueryKey(chatId ?? "");
const { data: fetchedNotifications = [] } = useQuery({
queryKey: [chatNotificationsQueryKey],
enabled: notifications.length > 0,
queryFn: () => {
if (chatId === undefined) {
return [];
}

return getChatNotifications(chatId);
},
refetchInterval: () => {
if (notifications.length === 0) {
return false;
}
const hasAPendingNotification = notifications.find(
(item) => item.status === "Pending"
);

if (hasAPendingNotification) {
//30 seconds
return 30_000;
}

return false;
},
});

useEffect(() => {
if (fetchedNotifications.length === 0) {
return;
}
setNotifications(fetchedNotifications);
}, [fetchedNotifications]);

useEffect(() => {
const fetchHistory = async () => {
if (chatId === undefined) {
Expand All @@ -30,5 +69,5 @@ export const useSelectedChatPage = () => {
setNotifications(getNotificationsFromChatItems(chatItems));
};
void fetchHistory();
}, [chatId, setMessages]);
}, [chatId]);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export const getChatNotificationsQueryKey = (chatId: string): string =>
`notifications-${chatId}`;
34 changes: 22 additions & 12 deletions frontend/app/chat/components/ChatsList/__tests__/ChatsList.test.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
/* eslint-disable max-lines */
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { act, fireEvent, render, screen } from "@testing-library/react";
import { afterEach, describe, expect, it, vi } from "vitest";

Expand All @@ -11,6 +12,7 @@ import * as useChatsListModule from "../hooks/useChatsList";
import { ChatsList } from "../index";

const getChatsMock = vi.fn(() => []);
const queryClient = new QueryClient();

const setOpenMock = vi.fn();

Expand Down Expand Up @@ -56,9 +58,11 @@ describe("ChatsList", () => {

it("should render correctly", () => {
const { getByTestId } = render(
<ChatProviderMock>
<ChatsList />
</ChatProviderMock>
<QueryClientProvider client={queryClient}>
<ChatProviderMock>
<ChatsList />
</ChatProviderMock>
</QueryClientProvider>
);
const chatsList = getByTestId("chats-list");
expect(chatsList).toBeDefined();
Expand All @@ -72,9 +76,11 @@ describe("ChatsList", () => {

it("renders the chats list with correct number of items", () => {
render(
<ChatProviderMock>
<ChatsList />
</ChatProviderMock>
<QueryClientProvider client={queryClient}>
<ChatProviderMock>
<ChatsList />
</ChatProviderMock>
</QueryClientProvider>
);
const chatItems = screen.getAllByTestId("chats-list-item");
expect(chatItems).toHaveLength(2);
Expand All @@ -88,9 +94,11 @@ describe("ChatsList", () => {

await act(() =>
render(
<ChatProviderMock>
(<ChatsList />)
</ChatProviderMock>
<QueryClientProvider client={queryClient}>
<ChatProviderMock>
(<ChatsList />)
</ChatProviderMock>
</QueryClientProvider>
)
);

Expand All @@ -109,9 +117,11 @@ describe("ChatsList", () => {
}));
await act(() =>
render(
<ChatProviderMock>
<ChatsList />
</ChatProviderMock>
<QueryClientProvider client={queryClient}>
<ChatProviderMock>
<ChatsList />
</ChatProviderMock>
</QueryClientProvider>
)
);

Expand Down
28 changes: 28 additions & 0 deletions frontend/lib/api/notification/__tests__/useNotificationApi.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { renderHook } from "@testing-library/react";
import { describe, expect, it, vi } from "vitest";

import { useNotificationApi } from "../useNotificationApi";

const axiosGetMock = vi.fn(() => ({}));

vi.mock("@/lib/hooks", () => ({
useAxios: () => ({
axiosInstance: {
get: axiosGetMock,
},
}),
}));

describe("useNotificationApi", () => {
it("should call getChatNotifications with the correct parameters", async () => {
const chatId = "test-chat-id";
const {
result: {
current: { getChatNotifications },
},
} = renderHook(() => useNotificationApi());
await getChatNotifications(chatId);
expect(axiosGetMock).toHaveBeenCalledTimes(1);
expect(axiosGetMock).toHaveBeenCalledWith(`/notifications/${chatId}`);
});
});
11 changes: 11 additions & 0 deletions frontend/lib/api/notification/notification.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { AxiosInstance } from "axios";

import { Notification } from "@/app/chat/[chatId]/types";

export const getChatNotifications = async (
chatId: string,
axiosInstance: AxiosInstance
): Promise<Notification[]> => {
return (await axiosInstance.get<Notification[]>(`/notifications/${chatId}`))
.data;
};
13 changes: 13 additions & 0 deletions frontend/lib/api/notification/useNotificationApi.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { useAxios } from "@/lib/hooks";

import { getChatNotifications } from "./notification";

// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
export const useNotificationApi = () => {
const { axiosInstance } = useAxios();

return {
getChatNotifications: async (chatId: string) =>
await getChatNotifications(chatId, axiosInstance),
};
};
Loading