mirror of
https://github.com/danswer-ai/danswer.git
synced 2025-05-19 00:00:37 +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 { 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>
|
||||||
</>
|
</>
|
||||||
|
@ -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>
|
||||||
</>
|
</>
|
||||||
|
@ -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 { 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>
|
||||||
|
@ -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;
|
||||||
|
@ -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;
|
||||||
|
}
|
||||||
|
@ -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"
|
||||||
|
@ -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}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
|
@ -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}>
|
||||||
|
Loading…
x
Reference in New Issue
Block a user