FIx chat refresh + add stop button

This commit is contained in:
Weves 2023-12-28 20:02:59 -08:00 committed by Chris Weaver
parent 73483b5e09
commit cdf260b277
10 changed files with 342 additions and 297 deletions

View File

@ -6,6 +6,7 @@ import { FiRefreshCcw, FiSend, FiStopCircle } from "react-icons/fi";
import { AIMessage, HumanMessage } from "./message/Messages"; import { AIMessage, HumanMessage } from "./message/Messages";
import { AnswerPiecePacket, DanswerDocument } from "@/lib/search/interfaces"; import { AnswerPiecePacket, DanswerDocument } from "@/lib/search/interfaces";
import { import {
BackendChatSession,
BackendMessage, BackendMessage,
DocumentsResponse, DocumentsResponse,
Message, Message,
@ -22,6 +23,7 @@ import {
handleAutoScroll, handleAutoScroll,
handleChatFeedback, handleChatFeedback,
nameChatSession, nameChatSession,
processRawChatHistory,
sendMessage, sendMessage,
} from "./lib"; } from "./lib";
import { ThreeDots } from "react-loader-spinner"; import { ThreeDots } from "react-loader-spinner";
@ -33,7 +35,6 @@ import { useFilters } from "@/lib/hooks";
import { DocumentSet, ValidSources } from "@/lib/types"; import { DocumentSet, ValidSources } from "@/lib/types";
import { ChatFilters } from "./modifiers/ChatFilters"; import { ChatFilters } from "./modifiers/ChatFilters";
import { buildFilters } from "@/lib/search/utils"; import { buildFilters } from "@/lib/search/utils";
import { QA, SearchTypeSelector } from "./modifiers/SearchTypeSelector";
import { SelectedDocuments } from "./modifiers/SelectedDocuments"; import { SelectedDocuments } from "./modifiers/SelectedDocuments";
import { usePopup } from "@/components/admin/connectors/Popup"; import { usePopup } from "@/components/admin/connectors/Popup";
import { ResizableSection } from "@/components/resizable/ResizableSection"; import { ResizableSection } from "@/components/resizable/ResizableSection";
@ -44,7 +45,6 @@ const MAX_INPUT_HEIGHT = 200;
export const Chat = ({ export const Chat = ({
existingChatSessionId, existingChatSessionId,
existingChatSessionPersonaId, existingChatSessionPersonaId,
existingMessages,
availableSources, availableSources,
availableDocumentSets, availableDocumentSets,
availablePersonas, availablePersonas,
@ -53,7 +53,6 @@ export const Chat = ({
}: { }: {
existingChatSessionId: number | null; existingChatSessionId: number | null;
existingChatSessionPersonaId: number | undefined; existingChatSessionPersonaId: number | undefined;
existingMessages: Message[];
availableSources: ValidSources[]; availableSources: ValidSources[];
availableDocumentSets: DocumentSet[]; availableDocumentSets: DocumentSet[];
availablePersonas: Persona[]; availablePersonas: Persona[];
@ -63,18 +62,55 @@ export const Chat = ({
const router = useRouter(); const router = useRouter();
const { popup, setPopup } = usePopup(); 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<number | null>(
existingChatSessionId
);
const [message, setMessage] = useState(""); const [message, setMessage] = useState("");
const [messageHistory, setMessageHistory] = const [messageHistory, setMessageHistory] = useState<Message[]>([]);
useState<Message[]>(existingMessages);
const [isStreaming, setIsStreaming] = useState(false); const [isStreaming, setIsStreaming] = useState(false);
// for document display // for document display
// NOTE: -1 is a special designation that means the latest AI message // NOTE: -1 is a special designation that means the latest AI message
const [selectedMessageForDocDisplay, setSelectedMessageForDocDisplay] = const [selectedMessageForDocDisplay, setSelectedMessageForDocDisplay] =
useState<number | null>( useState<number | null>(null);
messageHistory[messageHistory.length - 1]?.messageId || null
);
const { aiMessage } = selectedMessageForDocDisplay const { aiMessage } = selectedMessageForDocDisplay
? getHumanAndAIMessageFromMessageNumber( ? getHumanAndAIMessageFromMessageNumber(
messageHistory, messageHistory,
@ -95,8 +131,6 @@ export const Chat = ({
const filterManager = useFilters(); const filterManager = useFilters();
const [selectedSearchType, setSelectedSearchType] = useState(QA);
// state for cancelling streaming // state for cancelling streaming
const [isCancelled, setIsCancelled] = useState(false); const [isCancelled, setIsCancelled] = useState(false);
const isCancelledRef = useRef(isCancelled); const isCancelledRef = useRef(isCancelled);
@ -124,12 +158,7 @@ export const Chat = ({
useEffect(() => { useEffect(() => {
endDivRef.current?.scrollIntoView(); endDivRef.current?.scrollIntoView();
setHasPerformedInitialScroll(true); setHasPerformedInitialScroll(true);
}, []); }, [isFetchingChatMessages]);
// handle refreshes of the server-side props
useEffect(() => {
setMessageHistory(existingMessages);
}, [existingMessages]);
// handle re-sizing of the text area // handle re-sizing of the text area
const textareaRef = useRef<HTMLTextAreaElement>(null); const textareaRef = useRef<HTMLTextAreaElement>(null);
@ -215,9 +244,7 @@ export const Chat = ({
message: currMessage, message: currMessage,
parentMessageId: lastSuccessfulMessageId, parentMessageId: lastSuccessfulMessageId,
chatSessionId: currChatSessionId, chatSessionId: currChatSessionId,
// if search-only set prompt to null to tell backend to not give an answer promptId: 0,
promptId:
selectedSearchType === QA ? selectedPersona?.prompts[0]?.id : null,
filters: buildFilters( filters: buildFilters(
filterManager.selectedSources, filterManager.selectedSources,
filterManager.selectedDocumentSets, filterManager.selectedDocumentSets,
@ -292,7 +319,7 @@ export const Chat = ({
setSelectedMessageForDocDisplay(finalMessage.message_id); setSelectedMessageForDocDisplay(finalMessage.message_id);
} }
await nameChatSession(currChatSessionId, currMessage); await nameChatSession(currChatSessionId, currMessage);
router.push(`/chat/${currChatSessionId}?shouldhideBeforeScroll=true`, { router.push(`/chat?chatId=${currChatSessionId}`, {
scroll: false, scroll: false,
}); });
} }
@ -372,7 +399,9 @@ export const Chat = ({
</div> </div>
)} )}
{messageHistory.length === 0 && !isStreaming && ( {messageHistory.length === 0 &&
!isFetchingChatMessages &&
!isStreaming && (
<div className="flex justify-center items-center h-full"> <div className="flex justify-center items-center h-full">
<div className="px-8 w-searchbar-xs 2xl:w-searchbar-sm 3xl:w-searchbar"> <div className="px-8 w-searchbar-xs 2xl:w-searchbar-sm 3xl:w-searchbar">
<div className="flex"> <div className="flex">
@ -530,13 +559,6 @@ export const Chat = ({
<div className="flex"> <div className="flex">
<div className="w-searchbar-xs 2xl:w-searchbar-sm 3xl:w-searchbar mx-auto px-4 pt-1 flex"> <div className="w-searchbar-xs 2xl:w-searchbar-sm 3xl:w-searchbar mx-auto px-4 pt-1 flex">
<div className="mr-3">
<SearchTypeSelector
selectedSearchType={selectedSearchType}
setSelectedSearchType={setSelectedSearchType}
/>
</div>
{selectedDocuments.length > 0 ? ( {selectedDocuments.length > 0 ? (
<SelectedDocuments <SelectedDocuments
selectedDocuments={selectedDocuments} selectedDocuments={selectedDocuments}
@ -588,7 +610,11 @@ export const Chat = ({
value={message} value={message}
onChange={(e) => setMessage(e.target.value)} onChange={(e) => setMessage(e.target.value)}
onKeyDown={(event) => { onKeyDown={(event) => {
if (event.key === "Enter" && !event.shiftKey) { if (
event.key === "Enter" &&
!event.shiftKey &&
message
) {
onSubmit(); onSubmit();
event.preventDefault(); event.preventDefault();
} }
@ -598,8 +624,24 @@ export const Chat = ({
<div className="absolute bottom-4 right-10"> <div className="absolute bottom-4 right-10">
<div <div
className={"cursor-pointer"} className={"cursor-pointer"}
onClick={() => onSubmit()} onClick={() => {
if (!isStreaming) {
if (message) {
onSubmit();
}
} else {
setIsCancelled(true);
}
}}
> >
{isStreaming ? (
<FiStopCircle
size={18}
className={
"text-emphasis w-9 h-9 p-2 rounded-lg hover:bg-hover"
}
/>
) : (
<FiSend <FiSend
size={18} size={18}
className={ className={
@ -607,6 +649,7 @@ export const Chat = ({
(message ? "bg-blue-200" : "") (message ? "bg-blue-200" : "")
} }
/> />
)}
</div> </div>
</div> </div>
</div> </div>
@ -624,6 +667,7 @@ export const Chat = ({
selectedMessage={aiMessage} selectedMessage={aiMessage}
selectedDocuments={selectedDocuments} selectedDocuments={selectedDocuments}
setSelectedDocuments={setSelectedDocuments} setSelectedDocuments={setSelectedDocuments}
isLoading={isFetchingChatMessages}
/> />
</ResizableSection> </ResizableSection>
</> </>

View File

@ -1,226 +1,47 @@
import { "use client";
AuthTypeMetadata,
getAuthTypeMetadataSS, import { useSearchParams } from "next/navigation";
getCurrentUserSS, import { ChatSession } from "./interfaces";
} from "@/lib/userSS";
import { redirect } from "next/navigation";
import { fetchSS } from "@/lib/utilsSS";
import { Connector, DocumentSet, User, ValidSources } from "@/lib/types";
import { ChatSidebar } from "./sessionSidebar/ChatSidebar"; import { ChatSidebar } from "./sessionSidebar/ChatSidebar";
import { Chat } from "./Chat"; import { Chat } from "./Chat";
import { import { DocumentSet, User, ValidSources } from "@/lib/types";
BackendMessage,
ChatSession,
Message,
RetrievalType,
} from "./interfaces";
import { unstable_noStore as noStore } from "next/cache";
import { Persona } from "../admin/personas/interfaces"; 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({ export function ChatLayout({
chatId, user,
shouldhideBeforeScroll, chatSessions,
availableSources,
availableDocumentSets,
availablePersonas,
documentSidebarInitialWidth,
}: { }: {
chatId: string | null; user: User | null;
shouldhideBeforeScroll?: boolean; chatSessions: ChatSession[];
availableSources: ValidSources[];
availableDocumentSets: DocumentSet[];
availablePersonas: Persona[];
documentSidebarInitialWidth?: number;
}) { }) {
noStore(); const searchParams = useSearchParams();
const chatIdRaw = searchParams.get("chatId");
const currentChatId = chatId ? parseInt(chatId) : null; const chatId = chatIdRaw ? parseInt(chatIdRaw) : 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<any>[] = [];
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<number, BackendMessage> = 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;
return ( return (
<> <>
<InstantSSRAutoRefresh />
<ApiKeyModal />
{connectors.length === 0 && connectorsResponse?.ok && <WelcomeModal />}
<div className="flex relative bg-background text-default h-screen overflow-x-hidden"> <div className="flex relative bg-background text-default h-screen overflow-x-hidden">
<ChatSidebar <ChatSidebar
existingChats={chatSessions} existingChats={chatSessions}
currentChatId={currentChatId} currentChatId={chatId}
user={user} user={user}
/> />
<Chat <Chat
existingChatSessionId={currentChatId} existingChatSessionId={chatId}
existingChatSessionPersonaId={currentChatSession?.persona_id} existingChatSessionPersonaId={0}
existingMessages={messages}
availableSources={availableSources} availableSources={availableSources}
availableDocumentSets={documentSets} availableDocumentSets={availableDocumentSets}
availablePersonas={personas} availablePersonas={availablePersonas}
documentSidebarInitialWidth={finalDocumentSidebarInitialWidth} documentSidebarInitialWidth={documentSidebarInitialWidth}
shouldhideBeforeScroll={shouldhideBeforeScroll}
/> />
</div> </div>
</> </>

View File

@ -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",
});
}

View File

@ -2,7 +2,7 @@ import { DanswerDocument } from "@/lib/search/interfaces";
import { Text } from "@tremor/react"; import { Text } from "@tremor/react";
import { ChatDocumentDisplay } from "./ChatDocumentDisplay"; import { ChatDocumentDisplay } from "./ChatDocumentDisplay";
import { usePopup } from "@/components/admin/connectors/Popup"; 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 { SelectedDocumentDisplay } from "./SelectedDocumentDisplay";
import { removeDuplicateDocs } from "@/lib/documentUtils"; import { removeDuplicateDocs } from "@/lib/documentUtils";
import { BasicSelectable } from "@/components/BasicClickable"; import { BasicSelectable } from "@/components/BasicClickable";
@ -27,10 +27,12 @@ export function DocumentSidebar({
selectedMessage, selectedMessage,
selectedDocuments, selectedDocuments,
setSelectedDocuments, setSelectedDocuments,
isLoading,
}: { }: {
selectedMessage: Message | null; selectedMessage: Message | null;
selectedDocuments: DanswerDocument[] | null; selectedDocuments: DanswerDocument[] | null;
setSelectedDocuments: (documents: DanswerDocument[]) => void; setSelectedDocuments: (documents: DanswerDocument[]) => void;
isLoading: boolean;
}) { }) {
const { popup, setPopup } = usePopup(); const { popup, setPopup } = usePopup();
@ -115,12 +117,14 @@ export function DocumentSidebar({
</div> </div>
</div> </div>
) : ( ) : (
!isLoading && (
<div className="ml-4 mr-3"> <div className="ml-4 mr-3">
<Text> <Text>
When you run ask a question, the retrieved documents will show up When you run ask a question, the retrieved documents will show
here! up here!
</Text> </Text>
</div> </div>
)
)} )}
</div> </div>
@ -157,10 +161,12 @@ export function DocumentSidebar({
))} ))}
</div> </div>
) : ( ) : (
!isLoading && (
<Text className="mx-3 py-3"> <Text className="mx-3 py-3">
Select documents from the retrieved documents section to chat Select documents from the retrieved documents section to chat
specifically with them! specifically with them!
</Text> </Text>
)
)} )}
</div> </div>
</div> </div>

View File

@ -32,6 +32,10 @@ export interface Message {
citations?: CitationMap; citations?: CitationMap;
} }
export interface BackendChatSession {
messages: BackendMessage[];
}
export interface BackendMessage { export interface BackendMessage {
message_id: number; message_id: number;
parent_message: number | null; parent_message: number | null;

View File

@ -11,6 +11,7 @@ import {
ChatSession, ChatSession,
DocumentsResponse, DocumentsResponse,
Message, Message,
RetrievalType,
StreamingError, StreamingError,
} from "./interfaces"; } from "./interfaces";
@ -279,3 +280,62 @@ export function getLastSuccessfulMessageId(messageHistory: Message[]) {
); );
return lastSuccessfulMessage ? lastSuccessfulMessage?.messageId : null; return lastSuccessfulMessage ? lastSuccessfulMessage?.messageId : null;
} }
export function processRawChatHistory(rawMessages: BackendMessage[]) {
const messageMap: Map<number, BackendMessage> = 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;
}

View File

@ -116,9 +116,7 @@ export const AIMessage = ({
content content
)} )}
</> </>
) : isComplete ? ( ) : isComplete ? null : (
<div>I just performed the requested search!</div>
) : (
<div className="text-sm my-auto"> <div className="text-sm my-auto">
<ThreeDots <ThreeDots
height="30" height="30"

View File

@ -1,12 +1,137 @@
import ChatPage from "./ChatPage"; 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";
import {
BackendMessage,
ChatSession,
Message,
RetrievalType,
} from "./interfaces";
import { unstable_noStore as noStore } from "next/cache";
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";
import { ChatLayout } from "./ChatPage";
export default async function Page({ export default async function Page() {
searchParams, noStore();
}: {
searchParams: { shouldhideBeforeScroll?: string }; const tasks = [
}) { getAuthTypeMetadataSS(),
return await ChatPage({ getCurrentUserSS(),
chatId: null, fetchSS("/manage/connector"),
shouldhideBeforeScroll: searchParams.shouldhideBeforeScroll === "true", fetchSS("/manage/document-set"),
}); fetchSS("/persona?include_default=true"),
fetchSS("/chat/get-user-chat-sessions"),
];
// 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,
];
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 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<any>[] = [];
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 (
<>
<InstantSSRAutoRefresh />
<ApiKeyModal />
{connectors.length === 0 && <WelcomeModal />}
<ChatLayout
user={user}
chatSessions={chatSessions}
availableSources={availableSources}
availableDocumentSets={documentSets}
availablePersonas={personas}
documentSidebarInitialWidth={finalDocumentSidebarInitialWidth}
/>
</>
);
} }

View File

@ -18,6 +18,7 @@ import Image from "next/image";
import { ChatSessionDisplay } from "./SessionDisplay"; import { ChatSessionDisplay } from "./SessionDisplay";
import { ChatSession } from "../interfaces"; import { ChatSession } from "../interfaces";
import { groupSessionsByDateRange } from "../lib"; import { groupSessionsByDateRange } from "../lib";
interface ChatSidebarProps { interface ChatSidebarProps {
existingChats: ChatSession[]; existingChats: ChatSession[];
currentChatId: number | null; currentChatId: number | null;

View File

@ -52,7 +52,7 @@ export function ChatSessionDisplay({
<Link <Link
className="flex my-1" className="flex my-1"
key={chatSession.id} key={chatSession.id}
href={`/chat/${chatSession.id}`} href={`/chat?chatId=${chatSession.id}`}
scroll={false} scroll={false}
> >
<BasicSelectable fullWidth selected={isSelected}> <BasicSelectable fullWidth selected={isSelected}>