diff --git a/web/src/app/chat/Chat.tsx b/web/src/app/chat/Chat.tsx index 0911d0190..465355672 100644 --- a/web/src/app/chat/Chat.tsx +++ b/web/src/app/chat/Chat.tsx @@ -6,6 +6,7 @@ import { FiRefreshCcw, FiSend, FiStopCircle } from "react-icons/fi"; import { AIMessage, HumanMessage } from "./message/Messages"; import { AnswerPiecePacket, DanswerDocument } from "@/lib/search/interfaces"; import { + BackendChatSession, BackendMessage, DocumentsResponse, Message, @@ -22,6 +23,7 @@ import { handleAutoScroll, handleChatFeedback, nameChatSession, + processRawChatHistory, sendMessage, } from "./lib"; import { ThreeDots } from "react-loader-spinner"; @@ -33,7 +35,6 @@ import { useFilters } from "@/lib/hooks"; import { DocumentSet, ValidSources } from "@/lib/types"; import { ChatFilters } from "./modifiers/ChatFilters"; import { buildFilters } from "@/lib/search/utils"; -import { QA, SearchTypeSelector } from "./modifiers/SearchTypeSelector"; import { SelectedDocuments } from "./modifiers/SelectedDocuments"; import { usePopup } from "@/components/admin/connectors/Popup"; import { ResizableSection } from "@/components/resizable/ResizableSection"; @@ -44,7 +45,6 @@ const MAX_INPUT_HEIGHT = 200; export const Chat = ({ existingChatSessionId, existingChatSessionPersonaId, - existingMessages, availableSources, availableDocumentSets, availablePersonas, @@ -53,7 +53,6 @@ export const Chat = ({ }: { existingChatSessionId: number | null; existingChatSessionPersonaId: number | undefined; - existingMessages: Message[]; availableSources: ValidSources[]; availableDocumentSets: DocumentSet[]; availablePersonas: Persona[]; @@ -63,18 +62,55 @@ export const Chat = ({ const router = useRouter(); const { popup, setPopup } = usePopup(); - const [chatSessionId, setChatSessionId] = useState(existingChatSessionId); + // fetch messages for the chat session + const [isFetchingChatMessages, setIsFetchingChatMessages] = useState( + existingChatSessionId !== null + ); + + // this is triggered every time the user switches which chat + // session they are using + useEffect(() => { + textareaRef.current?.focus(); + setChatSessionId(existingChatSessionId); + + async function initialSessionFetch() { + if (existingChatSessionId === null) { + setIsFetchingChatMessages(false); + setMessageHistory([]); + return; + } + + setIsFetchingChatMessages(true); + const response = await fetch( + `/api/chat/get-chat-session/${existingChatSessionId}` + ); + const chatSession = (await response.json()) as BackendChatSession; + const newMessageHistory = processRawChatHistory(chatSession.messages); + setMessageHistory(newMessageHistory); + + const latestMessageId = + newMessageHistory[newMessageHistory.length - 1]?.messageId; + setSelectedMessageForDocDisplay( + latestMessageId !== undefined ? latestMessageId : null + ); + + setIsFetchingChatMessages(false); + } + + initialSessionFetch(); + }, [existingChatSessionId]); + + const [chatSessionId, setChatSessionId] = useState( + existingChatSessionId + ); const [message, setMessage] = useState(""); - const [messageHistory, setMessageHistory] = - useState(existingMessages); + const [messageHistory, setMessageHistory] = useState([]); const [isStreaming, setIsStreaming] = useState(false); // for document display // NOTE: -1 is a special designation that means the latest AI message const [selectedMessageForDocDisplay, setSelectedMessageForDocDisplay] = - useState( - messageHistory[messageHistory.length - 1]?.messageId || null - ); + useState(null); const { aiMessage } = selectedMessageForDocDisplay ? getHumanAndAIMessageFromMessageNumber( messageHistory, @@ -95,8 +131,6 @@ export const Chat = ({ const filterManager = useFilters(); - const [selectedSearchType, setSelectedSearchType] = useState(QA); - // state for cancelling streaming const [isCancelled, setIsCancelled] = useState(false); const isCancelledRef = useRef(isCancelled); @@ -124,12 +158,7 @@ export const Chat = ({ useEffect(() => { endDivRef.current?.scrollIntoView(); setHasPerformedInitialScroll(true); - }, []); - - // handle refreshes of the server-side props - useEffect(() => { - setMessageHistory(existingMessages); - }, [existingMessages]); + }, [isFetchingChatMessages]); // handle re-sizing of the text area const textareaRef = useRef(null); @@ -215,9 +244,7 @@ export const Chat = ({ message: currMessage, parentMessageId: lastSuccessfulMessageId, chatSessionId: currChatSessionId, - // if search-only set prompt to null to tell backend to not give an answer - promptId: - selectedSearchType === QA ? selectedPersona?.prompts[0]?.id : null, + promptId: 0, filters: buildFilters( filterManager.selectedSources, filterManager.selectedDocumentSets, @@ -292,7 +319,7 @@ export const Chat = ({ setSelectedMessageForDocDisplay(finalMessage.message_id); } await nameChatSession(currChatSessionId, currMessage); - router.push(`/chat/${currChatSessionId}?shouldhideBeforeScroll=true`, { + router.push(`/chat?chatId=${currChatSessionId}`, { scroll: false, }); } @@ -372,25 +399,27 @@ export const Chat = ({ )} - {messageHistory.length === 0 && !isStreaming && ( -
-
-
-
- Logo + {messageHistory.length === 0 && + !isFetchingChatMessages && + !isStreaming && ( +
+
+
+
+ Logo +
+
+
+ What are you looking for today?
-
- What are you looking for today? -
-
- )} + )}
-
- -
- {selectedDocuments.length > 0 ? ( setMessage(e.target.value)} onKeyDown={(event) => { - if (event.key === "Enter" && !event.shiftKey) { + if ( + event.key === "Enter" && + !event.shiftKey && + message + ) { onSubmit(); event.preventDefault(); } @@ -598,15 +624,32 @@ export const Chat = ({
onSubmit()} - > - { + if (!isStreaming) { + if (message) { + onSubmit(); + } + } else { + setIsCancelled(true); } - /> + }} + > + {isStreaming ? ( + + ) : ( + + )}
@@ -624,6 +667,7 @@ export const Chat = ({ selectedMessage={aiMessage} selectedDocuments={selectedDocuments} setSelectedDocuments={setSelectedDocuments} + isLoading={isFetchingChatMessages} /> diff --git a/web/src/app/chat/ChatPage.tsx b/web/src/app/chat/ChatPage.tsx index 8d9f5f6dd..a8fdd6f1d 100644 --- a/web/src/app/chat/ChatPage.tsx +++ b/web/src/app/chat/ChatPage.tsx @@ -1,226 +1,47 @@ -import { - AuthTypeMetadata, - getAuthTypeMetadataSS, - getCurrentUserSS, -} from "@/lib/userSS"; -import { redirect } from "next/navigation"; -import { fetchSS } from "@/lib/utilsSS"; -import { Connector, DocumentSet, User, ValidSources } from "@/lib/types"; +"use client"; + +import { useSearchParams } from "next/navigation"; +import { ChatSession } from "./interfaces"; import { ChatSidebar } from "./sessionSidebar/ChatSidebar"; import { Chat } from "./Chat"; -import { - BackendMessage, - ChatSession, - Message, - RetrievalType, -} from "./interfaces"; -import { unstable_noStore as noStore } from "next/cache"; +import { DocumentSet, User, ValidSources } from "@/lib/types"; import { Persona } from "../admin/personas/interfaces"; -import { InstantSSRAutoRefresh } from "@/components/SSRAutoRefresh"; -import { WelcomeModal } from "@/components/WelcomeModal"; -import { ApiKeyModal } from "@/components/openai/ApiKeyModal"; -import { cookies } from "next/headers"; -import { DOCUMENT_SIDEBAR_WIDTH_COOKIE_NAME } from "@/components/resizable/contants"; -import { personaComparator } from "../admin/personas/lib"; -export default async function ChatPage({ - chatId, - shouldhideBeforeScroll, +export function ChatLayout({ + user, + chatSessions, + availableSources, + availableDocumentSets, + availablePersonas, + documentSidebarInitialWidth, }: { - chatId: string | null; - shouldhideBeforeScroll?: boolean; + user: User | null; + chatSessions: ChatSession[]; + availableSources: ValidSources[]; + availableDocumentSets: DocumentSet[]; + availablePersonas: Persona[]; + documentSidebarInitialWidth?: number; }) { - noStore(); - - const currentChatId = chatId ? parseInt(chatId) : null; - - const tasks = [ - getAuthTypeMetadataSS(), - getCurrentUserSS(), - fetchSS("/manage/connector"), - fetchSS("/manage/document-set"), - fetchSS("/persona?include_default=true"), - fetchSS("/chat/get-user-chat-sessions"), - chatId !== null - ? fetchSS(`/chat/get-chat-session/${chatId}`) - : (async () => null)(), - ]; - - // catch cases where the backend is completely unreachable here - // without try / catch, will just raise an exception and the page - // will not render - let results: (User | Response | AuthTypeMetadata | null)[] = [ - null, - null, - null, - null, - null, - null, - null, - ]; - try { - results = await Promise.all(tasks); - } catch (e) { - console.log(`Some fetch failed for the main search page - ${e}`); - } - const authTypeMetadata = results[0] as AuthTypeMetadata | null; - const user = results[1] as User | null; - const connectorsResponse = results[2] as Response | null; - const documentSetsResponse = results[3] as Response | null; - const personasResponse = results[4] as Response | null; - const chatSessionsResponse = results[5] as Response | null; - const chatSessionMessagesResponse = results[6] as Response | null; - - const authDisabled = authTypeMetadata?.authType === "disabled"; - if (!authDisabled && !user) { - return redirect("/auth/login"); - } - - if (user && !user.is_verified && authTypeMetadata?.requiresVerification) { - return redirect("/auth/waiting-on-verification"); - } - - let connectors: Connector[] = []; - if (connectorsResponse?.ok) { - connectors = await connectorsResponse.json(); - } else { - console.log(`Failed to fetch connectors - ${connectorsResponse?.status}`); - } - const availableSources: ValidSources[] = []; - connectors.forEach((connector) => { - if (!availableSources.includes(connector.source)) { - availableSources.push(connector.source); - } - }); - - let chatSessions: ChatSession[] = []; - if (chatSessionsResponse?.ok) { - chatSessions = (await chatSessionsResponse.json()).sessions; - } else { - console.log( - `Failed to fetch chat sessions - ${chatSessionsResponse?.text()}` - ); - } - // Larger ID -> created later - chatSessions.sort((a, b) => (a.id > b.id ? -1 : 1)); - const currentChatSession = chatSessions.find( - (chatSession) => chatSession.id === currentChatId - ); - - let documentSets: DocumentSet[] = []; - if (documentSetsResponse?.ok) { - documentSets = await documentSetsResponse.json(); - } else { - console.log( - `Failed to fetch document sets - ${documentSetsResponse?.status}` - ); - } - - let personas: Persona[] = []; - if (personasResponse?.ok) { - personas = await personasResponse.json(); - } else { - console.log(`Failed to fetch personas - ${personasResponse?.status}`); - } - // remove those marked as hidden by an admin - personas = personas.filter((persona) => persona.is_visible); - // sort them in priority order - personas.sort(personaComparator); - - let messages: Message[] = []; - if (chatSessionMessagesResponse?.ok) { - const chatSessionDetailJson = await chatSessionMessagesResponse.json(); - const rawMessages = chatSessionDetailJson.messages as BackendMessage[]; - const messageMap: Map = new Map( - rawMessages.map((message) => [message.message_id, message]) - ); - - const rootMessage = rawMessages.find( - (message) => message.parent_message === null - ); - - const finalMessageList: BackendMessage[] = []; - if (rootMessage) { - let currMessage: BackendMessage | null = rootMessage; - while (currMessage) { - finalMessageList.push(currMessage); - const childMessageNumber = currMessage.latest_child_message; - if (childMessageNumber && messageMap.has(childMessageNumber)) { - currMessage = messageMap.get(childMessageNumber) as BackendMessage; - } else { - currMessage = null; - } - } - } - - messages = finalMessageList - .filter((messageInfo) => messageInfo.message_type !== "system") - .map((messageInfo) => { - const hasContextDocs = - (messageInfo?.context_docs?.top_documents || []).length > 0; - let retrievalType; - if (hasContextDocs) { - if (messageInfo.rephrased_query) { - retrievalType = RetrievalType.Search; - } else { - retrievalType = RetrievalType.SelectedDocs; - } - } else { - retrievalType = RetrievalType.None; - } - - return { - messageId: messageInfo.message_id, - message: messageInfo.message, - type: messageInfo.message_type as "user" | "assistant", - // only include these fields if this is an assistant message so that - // this is identical to what is computed at streaming time - ...(messageInfo.message_type === "assistant" - ? { - retrievalType: retrievalType, - query: messageInfo.rephrased_query, - documents: messageInfo?.context_docs?.top_documents || [], - citations: messageInfo?.citations || {}, - } - : {}), - }; - }); - } else { - console.log( - `Failed to fetch chat session messages - ${chatSessionMessagesResponse?.text()}` - ); - } - - const documentSidebarCookieInitialWidth = cookies().get( - DOCUMENT_SIDEBAR_WIDTH_COOKIE_NAME - ); - const finalDocumentSidebarInitialWidth = documentSidebarCookieInitialWidth - ? parseInt(documentSidebarCookieInitialWidth.value) - : undefined; + const searchParams = useSearchParams(); + const chatIdRaw = searchParams.get("chatId"); + const chatId = chatIdRaw ? parseInt(chatIdRaw) : null; return ( <> - - - - {connectors.length === 0 && connectorsResponse?.ok && } -
diff --git a/web/src/app/chat/[chatId]/page.tsx b/web/src/app/chat/[chatId]/page.tsx deleted file mode 100644 index f1649a741..000000000 --- a/web/src/app/chat/[chatId]/page.tsx +++ /dev/null @@ -1,14 +0,0 @@ -import ChatPage from "../ChatPage"; - -export default async function Page({ - params, - searchParams, -}: { - params: { chatId: string }; - searchParams: { shouldhideBeforeScroll?: string }; -}) { - return await ChatPage({ - chatId: params.chatId, - shouldhideBeforeScroll: searchParams.shouldhideBeforeScroll === "true", - }); -} diff --git a/web/src/app/chat/documentSidebar/DocumentSidebar.tsx b/web/src/app/chat/documentSidebar/DocumentSidebar.tsx index 5f7f4fd35..1b1082d47 100644 --- a/web/src/app/chat/documentSidebar/DocumentSidebar.tsx +++ b/web/src/app/chat/documentSidebar/DocumentSidebar.tsx @@ -2,7 +2,7 @@ import { DanswerDocument } from "@/lib/search/interfaces"; import { Text } from "@tremor/react"; import { ChatDocumentDisplay } from "./ChatDocumentDisplay"; import { usePopup } from "@/components/admin/connectors/Popup"; -import { FiFileText, FiSearch } from "react-icons/fi"; +import { FiFileText } from "react-icons/fi"; import { SelectedDocumentDisplay } from "./SelectedDocumentDisplay"; import { removeDuplicateDocs } from "@/lib/documentUtils"; import { BasicSelectable } from "@/components/BasicClickable"; @@ -27,10 +27,12 @@ export function DocumentSidebar({ selectedMessage, selectedDocuments, setSelectedDocuments, + isLoading, }: { selectedMessage: Message | null; selectedDocuments: DanswerDocument[] | null; setSelectedDocuments: (documents: DanswerDocument[]) => void; + isLoading: boolean; }) { const { popup, setPopup } = usePopup(); @@ -115,12 +117,14 @@ export function DocumentSidebar({
) : ( -
- - When you run ask a question, the retrieved documents will show up - here! - -
+ !isLoading && ( +
+ + When you run ask a question, the retrieved documents will show + up here! + +
+ ) )}
@@ -157,10 +161,12 @@ export function DocumentSidebar({ ))}
) : ( - - Select documents from the retrieved documents section to chat - specifically with them! - + !isLoading && ( + + Select documents from the retrieved documents section to chat + specifically with them! + + ) )} diff --git a/web/src/app/chat/interfaces.ts b/web/src/app/chat/interfaces.ts index 3bd985161..0f612000b 100644 --- a/web/src/app/chat/interfaces.ts +++ b/web/src/app/chat/interfaces.ts @@ -32,6 +32,10 @@ export interface Message { citations?: CitationMap; } +export interface BackendChatSession { + messages: BackendMessage[]; +} + export interface BackendMessage { message_id: number; parent_message: number | null; diff --git a/web/src/app/chat/lib.tsx b/web/src/app/chat/lib.tsx index 849f58a4f..aab2714e2 100644 --- a/web/src/app/chat/lib.tsx +++ b/web/src/app/chat/lib.tsx @@ -11,6 +11,7 @@ import { ChatSession, DocumentsResponse, Message, + RetrievalType, StreamingError, } from "./interfaces"; @@ -279,3 +280,62 @@ export function getLastSuccessfulMessageId(messageHistory: Message[]) { ); return lastSuccessfulMessage ? lastSuccessfulMessage?.messageId : null; } + +export function processRawChatHistory(rawMessages: BackendMessage[]) { + const messageMap: Map = new Map( + rawMessages.map((message) => [message.message_id, message]) + ); + + const rootMessage = rawMessages.find( + (message) => message.parent_message === null + ); + + const finalMessageList: BackendMessage[] = []; + if (rootMessage) { + let currMessage: BackendMessage | null = rootMessage; + while (currMessage) { + finalMessageList.push(currMessage); + const childMessageNumber = currMessage.latest_child_message; + if (childMessageNumber && messageMap.has(childMessageNumber)) { + currMessage = messageMap.get(childMessageNumber) as BackendMessage; + } else { + currMessage = null; + } + } + } + + const messages: Message[] = finalMessageList + .filter((messageInfo) => messageInfo.message_type !== "system") + .map((messageInfo) => { + const hasContextDocs = + (messageInfo?.context_docs?.top_documents || []).length > 0; + let retrievalType; + if (hasContextDocs) { + if (messageInfo.rephrased_query) { + retrievalType = RetrievalType.Search; + } else { + retrievalType = RetrievalType.SelectedDocs; + } + } else { + retrievalType = RetrievalType.None; + } + + return { + messageId: messageInfo.message_id, + message: messageInfo.message, + type: messageInfo.message_type as "user" | "assistant", + // only include these fields if this is an assistant message so that + // this is identical to what is computed at streaming time + ...(messageInfo.message_type === "assistant" + ? { + retrievalType: retrievalType, + query: messageInfo.rephrased_query, + documents: messageInfo?.context_docs?.top_documents || [], + citations: messageInfo?.citations || {}, + } + : {}), + }; + }); + + return messages; +} diff --git a/web/src/app/chat/message/Messages.tsx b/web/src/app/chat/message/Messages.tsx index 98f0dcbd6..9d3c2f2e4 100644 --- a/web/src/app/chat/message/Messages.tsx +++ b/web/src/app/chat/message/Messages.tsx @@ -116,9 +116,7 @@ export const AIMessage = ({ content )} - ) : isComplete ? ( -
I just performed the requested search!
- ) : ( + ) : isComplete ? null : (
[] = []; + if (connectorsResponse?.ok) { + connectors = await connectorsResponse.json(); + } else { + console.log(`Failed to fetch connectors - ${connectorsResponse?.status}`); + } + const availableSources: ValidSources[] = []; + connectors.forEach((connector) => { + if (!availableSources.includes(connector.source)) { + availableSources.push(connector.source); + } }); + + let chatSessions: ChatSession[] = []; + if (chatSessionsResponse?.ok) { + chatSessions = (await chatSessionsResponse.json()).sessions; + } else { + console.log( + `Failed to fetch chat sessions - ${chatSessionsResponse?.text()}` + ); + } + // Larger ID -> created later + chatSessions.sort((a, b) => (a.id > b.id ? -1 : 1)); + + let documentSets: DocumentSet[] = []; + if (documentSetsResponse?.ok) { + documentSets = await documentSetsResponse.json(); + } else { + console.log( + `Failed to fetch document sets - ${documentSetsResponse?.status}` + ); + } + + let personas: Persona[] = []; + if (personasResponse?.ok) { + personas = await personasResponse.json(); + } else { + console.log(`Failed to fetch personas - ${personasResponse?.status}`); + } + // remove those marked as hidden by an admin + personas = personas.filter((persona) => persona.is_visible); + // sort them in priority order + personas.sort(personaComparator); + + const documentSidebarCookieInitialWidth = cookies().get( + DOCUMENT_SIDEBAR_WIDTH_COOKIE_NAME + ); + const finalDocumentSidebarInitialWidth = documentSidebarCookieInitialWidth + ? parseInt(documentSidebarCookieInitialWidth.value) + : undefined; + + return ( + <> + + + + {connectors.length === 0 && } + + + + ); } diff --git a/web/src/app/chat/sessionSidebar/ChatSidebar.tsx b/web/src/app/chat/sessionSidebar/ChatSidebar.tsx index 0c7df136b..4d4b5ceb6 100644 --- a/web/src/app/chat/sessionSidebar/ChatSidebar.tsx +++ b/web/src/app/chat/sessionSidebar/ChatSidebar.tsx @@ -18,6 +18,7 @@ import Image from "next/image"; import { ChatSessionDisplay } from "./SessionDisplay"; import { ChatSession } from "../interfaces"; import { groupSessionsByDateRange } from "../lib"; + interface ChatSidebarProps { existingChats: ChatSession[]; currentChatId: number | null; diff --git a/web/src/app/chat/sessionSidebar/SessionDisplay.tsx b/web/src/app/chat/sessionSidebar/SessionDisplay.tsx index 93cd2025f..28eae4be2 100644 --- a/web/src/app/chat/sessionSidebar/SessionDisplay.tsx +++ b/web/src/app/chat/sessionSidebar/SessionDisplay.tsx @@ -52,7 +52,7 @@ export function ChatSessionDisplay({