mirror of
https://github.com/danswer-ai/danswer.git
synced 2025-05-16 23:00:31 +02:00
FIx chat refresh + add stop button
This commit is contained in:
parent
73483b5e09
commit
cdf260b277
@ -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<number | null>(
|
||||
existingChatSessionId
|
||||
);
|
||||
const [message, setMessage] = useState("");
|
||||
const [messageHistory, setMessageHistory] =
|
||||
useState<Message[]>(existingMessages);
|
||||
const [messageHistory, setMessageHistory] = useState<Message[]>([]);
|
||||
const [isStreaming, setIsStreaming] = useState(false);
|
||||
|
||||
// for document display
|
||||
// NOTE: -1 is a special designation that means the latest AI message
|
||||
const [selectedMessageForDocDisplay, setSelectedMessageForDocDisplay] =
|
||||
useState<number | null>(
|
||||
messageHistory[messageHistory.length - 1]?.messageId || null
|
||||
);
|
||||
useState<number | null>(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<HTMLTextAreaElement>(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 = ({
|
||||
</div>
|
||||
)}
|
||||
|
||||
{messageHistory.length === 0 && !isStreaming && (
|
||||
<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="flex">
|
||||
<div className="mx-auto h-[80px] w-[80px]">
|
||||
<Image
|
||||
src="/logo.png"
|
||||
alt="Logo"
|
||||
width="1419"
|
||||
height="1520"
|
||||
/>
|
||||
{messageHistory.length === 0 &&
|
||||
!isFetchingChatMessages &&
|
||||
!isStreaming && (
|
||||
<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="flex">
|
||||
<div className="mx-auto h-[80px] w-[80px]">
|
||||
<Image
|
||||
src="/logo.png"
|
||||
alt="Logo"
|
||||
width="1419"
|
||||
height="1520"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mx-auto text-2xl font-bold text-strong p-4 w-fit">
|
||||
What are you looking for today?
|
||||
</div>
|
||||
</div>
|
||||
<div className="mx-auto text-2xl font-bold text-strong p-4 w-fit">
|
||||
What are you looking for today?
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
)}
|
||||
|
||||
<div
|
||||
className={
|
||||
@ -530,13 +559,6 @@ export const Chat = ({
|
||||
|
||||
<div className="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
|
||||
selectedDocuments={selectedDocuments}
|
||||
@ -588,7 +610,11 @@ export const Chat = ({
|
||||
value={message}
|
||||
onChange={(e) => 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 = ({
|
||||
<div className="absolute bottom-4 right-10">
|
||||
<div
|
||||
className={"cursor-pointer"}
|
||||
onClick={() => onSubmit()}
|
||||
>
|
||||
<FiSend
|
||||
size={18}
|
||||
className={
|
||||
"text-emphasis w-9 h-9 p-2 rounded-lg " +
|
||||
(message ? "bg-blue-200" : "")
|
||||
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
|
||||
size={18}
|
||||
className={
|
||||
"text-emphasis w-9 h-9 p-2 rounded-lg " +
|
||||
(message ? "bg-blue-200" : "")
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -624,6 +667,7 @@ export const Chat = ({
|
||||
selectedMessage={aiMessage}
|
||||
selectedDocuments={selectedDocuments}
|
||||
setSelectedDocuments={setSelectedDocuments}
|
||||
isLoading={isFetchingChatMessages}
|
||||
/>
|
||||
</ResizableSection>
|
||||
</>
|
||||
|
@ -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<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;
|
||||
const searchParams = useSearchParams();
|
||||
const chatIdRaw = searchParams.get("chatId");
|
||||
const chatId = chatIdRaw ? parseInt(chatIdRaw) : null;
|
||||
|
||||
return (
|
||||
<>
|
||||
<InstantSSRAutoRefresh />
|
||||
<ApiKeyModal />
|
||||
|
||||
{connectors.length === 0 && connectorsResponse?.ok && <WelcomeModal />}
|
||||
|
||||
<div className="flex relative bg-background text-default h-screen overflow-x-hidden">
|
||||
<ChatSidebar
|
||||
existingChats={chatSessions}
|
||||
currentChatId={currentChatId}
|
||||
currentChatId={chatId}
|
||||
user={user}
|
||||
/>
|
||||
|
||||
<Chat
|
||||
existingChatSessionId={currentChatId}
|
||||
existingChatSessionPersonaId={currentChatSession?.persona_id}
|
||||
existingMessages={messages}
|
||||
existingChatSessionId={chatId}
|
||||
existingChatSessionPersonaId={0}
|
||||
availableSources={availableSources}
|
||||
availableDocumentSets={documentSets}
|
||||
availablePersonas={personas}
|
||||
documentSidebarInitialWidth={finalDocumentSidebarInitialWidth}
|
||||
shouldhideBeforeScroll={shouldhideBeforeScroll}
|
||||
availableDocumentSets={availableDocumentSets}
|
||||
availablePersonas={availablePersonas}
|
||||
documentSidebarInitialWidth={documentSidebarInitialWidth}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
|
@ -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",
|
||||
});
|
||||
}
|
@ -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({
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="ml-4 mr-3">
|
||||
<Text>
|
||||
When you run ask a question, the retrieved documents will show up
|
||||
here!
|
||||
</Text>
|
||||
</div>
|
||||
!isLoading && (
|
||||
<div className="ml-4 mr-3">
|
||||
<Text>
|
||||
When you run ask a question, the retrieved documents will show
|
||||
up here!
|
||||
</Text>
|
||||
</div>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
|
||||
@ -157,10 +161,12 @@ export function DocumentSidebar({
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<Text className="mx-3 py-3">
|
||||
Select documents from the retrieved documents section to chat
|
||||
specifically with them!
|
||||
</Text>
|
||||
!isLoading && (
|
||||
<Text className="mx-3 py-3">
|
||||
Select documents from the retrieved documents section to chat
|
||||
specifically with them!
|
||||
</Text>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
@ -32,6 +32,10 @@ export interface Message {
|
||||
citations?: CitationMap;
|
||||
}
|
||||
|
||||
export interface BackendChatSession {
|
||||
messages: BackendMessage[];
|
||||
}
|
||||
|
||||
export interface BackendMessage {
|
||||
message_id: number;
|
||||
parent_message: number | null;
|
||||
|
@ -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<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;
|
||||
}
|
||||
|
@ -116,9 +116,7 @@ export const AIMessage = ({
|
||||
content
|
||||
)}
|
||||
</>
|
||||
) : isComplete ? (
|
||||
<div>I just performed the requested search!</div>
|
||||
) : (
|
||||
) : isComplete ? null : (
|
||||
<div className="text-sm my-auto">
|
||||
<ThreeDots
|
||||
height="30"
|
||||
|
@ -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({
|
||||
searchParams,
|
||||
}: {
|
||||
searchParams: { shouldhideBeforeScroll?: string };
|
||||
}) {
|
||||
return await ChatPage({
|
||||
chatId: null,
|
||||
shouldhideBeforeScroll: searchParams.shouldhideBeforeScroll === "true",
|
||||
export default async function Page() {
|
||||
noStore();
|
||||
|
||||
const tasks = [
|
||||
getAuthTypeMetadataSS(),
|
||||
getCurrentUserSS(),
|
||||
fetchSS("/manage/connector"),
|
||||
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}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -52,7 +52,7 @@ export function ChatSessionDisplay({
|
||||
<Link
|
||||
className="flex my-1"
|
||||
key={chatSession.id}
|
||||
href={`/chat/${chatSession.id}`}
|
||||
href={`/chat?chatId=${chatSession.id}`}
|
||||
scroll={false}
|
||||
>
|
||||
<BasicSelectable fullWidth selected={isSelected}>
|
||||
|
Loading…
x
Reference in New Issue
Block a user