mirror of
https://github.com/danswer-ai/danswer.git
synced 2025-03-29 11:12:02 +01:00
Feature/scroll (#1694)
--------- Co-authored-by: “Pablo <“pablo@danswer.ai”>
This commit is contained in:
parent
2140f80891
commit
9310a8edc2
@ -26,9 +26,9 @@ import {
|
||||
getCitedDocumentsFromMessage,
|
||||
getHumanAndAIMessageFromMessageNumber,
|
||||
getLastSuccessfulMessageId,
|
||||
handleAutoScroll,
|
||||
handleChatFeedback,
|
||||
nameChatSession,
|
||||
PacketType,
|
||||
personaIncludesRetrieval,
|
||||
processRawChatHistory,
|
||||
removeMessage,
|
||||
@ -37,6 +37,7 @@ import {
|
||||
updateModelOverrideForChatSession,
|
||||
updateParentChildren,
|
||||
uploadFilesForChat,
|
||||
useScrollonStream,
|
||||
} from "./lib";
|
||||
import { useContext, useEffect, useRef, useState } from "react";
|
||||
import { usePopup } from "@/components/admin/connectors/Popup";
|
||||
@ -50,7 +51,7 @@ import { DanswerInitializingLoader } from "@/components/DanswerInitializingLoade
|
||||
import { FeedbackModal } from "./modal/FeedbackModal";
|
||||
import { ShareChatSessionModal } from "./modal/ShareChatSessionModal";
|
||||
import { ChatPersonaSelector } from "./ChatPersonaSelector";
|
||||
import { FiShare2 } from "react-icons/fi";
|
||||
import { FiArrowDown, FiShare2 } from "react-icons/fi";
|
||||
import { ChatIntro } from "./ChatIntro";
|
||||
import { AIMessage, HumanMessage } from "./message/Messages";
|
||||
import { ThreeDots } from "react-loader-spinner";
|
||||
@ -77,6 +78,7 @@ import { TbLayoutSidebarRightExpand } from "react-icons/tb";
|
||||
import { SIDEBAR_WIDTH_CONST } from "@/lib/constants";
|
||||
|
||||
import ResizableSection from "@/components/resizable/ResizableSection";
|
||||
import { Button } from "@tremor/react";
|
||||
|
||||
const MAX_INPUT_HEIGHT = 200;
|
||||
const TEMP_USER_MESSAGE_ID = -1;
|
||||
@ -426,49 +428,111 @@ export function ChatPage({
|
||||
availableDocumentSets,
|
||||
});
|
||||
|
||||
const [currentFeedback, setCurrentFeedback] = useState<
|
||||
[FeedbackType, number] | null
|
||||
>(null);
|
||||
|
||||
const [sharingModalVisible, setSharingModalVisible] =
|
||||
useState<boolean>(false);
|
||||
|
||||
// state for cancelling streaming
|
||||
const [isCancelled, setIsCancelled] = useState(false);
|
||||
const isCancelledRef = useRef(isCancelled);
|
||||
const [aboveHorizon, setAboveHorizon] = useState(false);
|
||||
|
||||
const scrollableDivRef = useRef<HTMLDivElement>(null);
|
||||
const lastMessageRef = useRef<HTMLDivElement>(null);
|
||||
const inputRef = useRef<HTMLDivElement>(null);
|
||||
const endDivRef = useRef<HTMLDivElement>(null);
|
||||
const endPaddingRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const previousHeight = useRef<number>(
|
||||
inputRef.current?.getBoundingClientRect().height!
|
||||
);
|
||||
const scrollDist = useRef<number>(0);
|
||||
|
||||
const updateScrollTracking = () => {
|
||||
const scrollDistance =
|
||||
endDivRef?.current?.getBoundingClientRect()?.top! -
|
||||
inputRef?.current?.getBoundingClientRect()?.top!;
|
||||
scrollDist.current = scrollDistance;
|
||||
setAboveHorizon(scrollDist.current > 500);
|
||||
};
|
||||
|
||||
scrollableDivRef?.current?.addEventListener("scroll", updateScrollTracking);
|
||||
|
||||
const handleInputResize = () => {
|
||||
setTimeout(() => {
|
||||
if (inputRef.current && lastMessageRef.current) {
|
||||
let newHeight: number =
|
||||
inputRef.current?.getBoundingClientRect().height!;
|
||||
const heightDifference = newHeight - previousHeight.current;
|
||||
if (
|
||||
previousHeight.current &&
|
||||
heightDifference != 0 &&
|
||||
endPaddingRef.current &&
|
||||
scrollableDivRef &&
|
||||
scrollableDivRef.current
|
||||
) {
|
||||
endPaddingRef.current.style.transition = "height 0.3s ease-out";
|
||||
endPaddingRef.current.style.height = `${Math.max(newHeight - 50, 0)}px`;
|
||||
|
||||
scrollableDivRef?.current.scrollBy({
|
||||
left: 0,
|
||||
top: Math.max(heightDifference, 0),
|
||||
behavior: "smooth",
|
||||
});
|
||||
}
|
||||
previousHeight.current = newHeight;
|
||||
}
|
||||
}, 100);
|
||||
};
|
||||
|
||||
const clientScrollToBottom = (fast?: boolean) => {
|
||||
setTimeout(
|
||||
() => {
|
||||
endDivRef.current?.scrollIntoView({ behavior: "smooth" });
|
||||
setHasPerformedInitialScroll(true);
|
||||
},
|
||||
fast ? 50 : 500
|
||||
);
|
||||
};
|
||||
|
||||
const isCancelledRef = useRef<boolean>(isCancelled); // scroll is cancelled
|
||||
useEffect(() => {
|
||||
isCancelledRef.current = isCancelled;
|
||||
}, [isCancelled]);
|
||||
|
||||
const [currentFeedback, setCurrentFeedback] = useState<
|
||||
[FeedbackType, number] | null
|
||||
>(null);
|
||||
const [sharingModalVisible, setSharingModalVisible] =
|
||||
useState<boolean>(false);
|
||||
const distance = 500; // distance that should "engage" the scroll
|
||||
const debounce = 100; // time for debouncing
|
||||
|
||||
// auto scroll as message comes out
|
||||
const scrollableDivRef = useRef<HTMLDivElement>(null);
|
||||
const endDivRef = useRef<HTMLDivElement>(null);
|
||||
useEffect(() => {
|
||||
if (isStreaming || !message) {
|
||||
handleAutoScroll(endDivRef, scrollableDivRef);
|
||||
}
|
||||
useScrollonStream({
|
||||
isStreaming,
|
||||
scrollableDivRef,
|
||||
scrollDist,
|
||||
endDivRef,
|
||||
distance,
|
||||
debounce,
|
||||
});
|
||||
|
||||
// scroll to bottom initially
|
||||
const [hasPerformedInitialScroll, setHasPerformedInitialScroll] =
|
||||
useState(false);
|
||||
|
||||
// on new page
|
||||
useEffect(() => {
|
||||
endDivRef.current?.scrollIntoView();
|
||||
setHasPerformedInitialScroll(true);
|
||||
}, [isFetchingChatMessages]);
|
||||
clientScrollToBottom();
|
||||
}, [chatSessionId]);
|
||||
|
||||
// handle re-sizing of the text area
|
||||
const textAreaRef = useRef<HTMLTextAreaElement>(null);
|
||||
useEffect(() => {
|
||||
const textarea = textAreaRef.current;
|
||||
if (textarea) {
|
||||
textarea.style.height = "0px";
|
||||
textarea.style.height = `${Math.min(
|
||||
textarea.scrollHeight,
|
||||
MAX_INPUT_HEIGHT
|
||||
)}px`;
|
||||
}
|
||||
handleInputResize();
|
||||
}, [message]);
|
||||
|
||||
// tracks scrolling
|
||||
useEffect(() => {
|
||||
updateScrollTracking();
|
||||
}, [messageHistory]);
|
||||
|
||||
// used for resizing of the document sidebar
|
||||
const masterFlexboxRef = useRef<HTMLDivElement>(null);
|
||||
const [maxDocumentSidebarWidth, setMaxDocumentSidebarWidth] = useState<
|
||||
@ -502,6 +566,53 @@ export function ChatPage({
|
||||
documentSidebarInitialWidth = Math.min(700, maxDocumentSidebarWidth);
|
||||
}
|
||||
|
||||
class CurrentMessageFIFO {
|
||||
private stack: PacketType[] = [];
|
||||
isComplete: boolean = false;
|
||||
error: string | null = null;
|
||||
|
||||
push(packetBunch: PacketType) {
|
||||
this.stack.push(packetBunch);
|
||||
}
|
||||
|
||||
nextPacket(): PacketType | undefined {
|
||||
return this.stack.shift();
|
||||
}
|
||||
|
||||
isEmpty(): boolean {
|
||||
return this.stack.length === 0;
|
||||
}
|
||||
}
|
||||
async function updateCurrentMessageFIFO(
|
||||
stack: CurrentMessageFIFO,
|
||||
params: any
|
||||
) {
|
||||
try {
|
||||
for await (const packetBunch of sendMessage(params)) {
|
||||
for (const packet of packetBunch) {
|
||||
stack.push(packet);
|
||||
}
|
||||
|
||||
if (isCancelledRef.current) {
|
||||
setIsCancelled(false);
|
||||
break;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
stack.error = String(error);
|
||||
} finally {
|
||||
stack.isComplete = true;
|
||||
}
|
||||
}
|
||||
|
||||
const resetInputBar = () => {
|
||||
setMessage("");
|
||||
setCurrentMessageFiles([]);
|
||||
if (endPaddingRef.current) {
|
||||
endPaddingRef.current.style.height = `95px`;
|
||||
}
|
||||
};
|
||||
|
||||
const onSubmit = async ({
|
||||
messageIdToResend,
|
||||
messageOverride,
|
||||
@ -515,6 +626,7 @@ export function ChatPage({
|
||||
forceSearch?: boolean;
|
||||
isSeededChat?: boolean;
|
||||
} = {}) => {
|
||||
clientScrollToBottom();
|
||||
let currChatSessionId: number;
|
||||
let isNewSession = chatSessionId === null;
|
||||
const searchParamBasedChatSessionName =
|
||||
@ -596,9 +708,7 @@ export function ChatPage({
|
||||
if (!parentMessage && frozenCompleteMessageMap.size === 2) {
|
||||
parentMessage = frozenCompleteMessageMap.get(SYSTEM_MESSAGE_ID) || null;
|
||||
}
|
||||
setMessage("");
|
||||
setCurrentMessageFiles([]);
|
||||
|
||||
resetInputBar();
|
||||
setIsStreaming(true);
|
||||
let answer = "";
|
||||
let query: string | null = null;
|
||||
@ -614,7 +724,9 @@ export function ChatPage({
|
||||
try {
|
||||
const lastSuccessfulMessageId =
|
||||
getLastSuccessfulMessageId(currMessageHistory);
|
||||
for await (const packetBunch of sendMessage({
|
||||
|
||||
const stack = new CurrentMessageFIFO();
|
||||
updateCurrentMessageFIFO(stack, {
|
||||
message: currMessage,
|
||||
fileDescriptors: currentMessageFiles,
|
||||
parentMessageId: lastSuccessfulMessageId,
|
||||
@ -647,86 +759,108 @@ export function ChatPage({
|
||||
systemPromptOverride:
|
||||
searchParams.get(SEARCH_PARAM_NAMES.SYSTEM_PROMPT) || undefined,
|
||||
useExistingUserMessage: isSeededChat,
|
||||
})) {
|
||||
for (const packet of packetBunch) {
|
||||
if (Object.hasOwn(packet, "answer_piece")) {
|
||||
answer += (packet as AnswerPiecePacket).answer_piece;
|
||||
} else if (Object.hasOwn(packet, "top_documents")) {
|
||||
documents = (packet as DocumentsResponse).top_documents;
|
||||
query = (packet as DocumentsResponse).rephrased_query;
|
||||
retrievalType = RetrievalType.Search;
|
||||
if (documents && documents.length > 0) {
|
||||
// point to the latest message (we don't know the messageId yet, which is why
|
||||
// we have to use -1)
|
||||
setSelectedMessageForDocDisplay(TEMP_USER_MESSAGE_ID);
|
||||
}
|
||||
} else if (Object.hasOwn(packet, "file_ids")) {
|
||||
aiMessageImages = (packet as ImageGenerationDisplay).file_ids.map(
|
||||
(fileId) => {
|
||||
return {
|
||||
id: fileId,
|
||||
type: ChatFileType.IMAGE,
|
||||
};
|
||||
});
|
||||
const updateFn = (messages: Message[]) => {
|
||||
const replacementsMap = finalMessage
|
||||
? new Map([
|
||||
[messages[0].messageId, TEMP_USER_MESSAGE_ID],
|
||||
[messages[1].messageId, TEMP_ASSISTANT_MESSAGE_ID],
|
||||
] as [number, number][])
|
||||
: null;
|
||||
upsertToCompleteMessageMap({
|
||||
messages: messages,
|
||||
replacementsMap: replacementsMap,
|
||||
completeMessageMapOverride: frozenCompleteMessageMap,
|
||||
});
|
||||
};
|
||||
const delay = (ms: number) => {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||
};
|
||||
|
||||
await delay(50);
|
||||
while (!stack.isComplete || !stack.isEmpty()) {
|
||||
await delay(2);
|
||||
|
||||
if (!stack.isEmpty()) {
|
||||
const packet = stack.nextPacket();
|
||||
|
||||
if (packet) {
|
||||
if (Object.hasOwn(packet, "answer_piece")) {
|
||||
answer += (packet as AnswerPiecePacket).answer_piece;
|
||||
} else if (Object.hasOwn(packet, "top_documents")) {
|
||||
documents = (packet as DocumentsResponse).top_documents;
|
||||
query = (packet as DocumentsResponse).rephrased_query;
|
||||
retrievalType = RetrievalType.Search;
|
||||
if (documents && documents.length > 0) {
|
||||
// point to the latest message (we don't know the messageId yet, which is why
|
||||
// we have to use -1)
|
||||
setSelectedMessageForDocDisplay(TEMP_USER_MESSAGE_ID);
|
||||
}
|
||||
);
|
||||
} else if (Object.hasOwn(packet, "tool_name")) {
|
||||
toolCalls = [
|
||||
} else if (Object.hasOwn(packet, "tool_name")) {
|
||||
toolCalls = [
|
||||
{
|
||||
tool_name: (packet as ToolCallMetadata).tool_name,
|
||||
tool_args: (packet as ToolCallMetadata).tool_args,
|
||||
tool_result: (packet as ToolCallMetadata).tool_result,
|
||||
},
|
||||
];
|
||||
} else if (Object.hasOwn(packet, "file_ids")) {
|
||||
aiMessageImages = (packet as ImageGenerationDisplay).file_ids.map(
|
||||
(fileId) => {
|
||||
return {
|
||||
id: fileId,
|
||||
type: ChatFileType.IMAGE,
|
||||
};
|
||||
}
|
||||
);
|
||||
} else if (Object.hasOwn(packet, "tool_name")) {
|
||||
toolCalls = [
|
||||
{
|
||||
tool_name: (packet as ToolCallMetadata).tool_name,
|
||||
tool_args: (packet as ToolCallMetadata).tool_args,
|
||||
tool_result: (packet as ToolCallMetadata).tool_result,
|
||||
},
|
||||
];
|
||||
} else if (Object.hasOwn(packet, "error")) {
|
||||
error = (packet as StreamingError).error;
|
||||
} else if (Object.hasOwn(packet, "message_id")) {
|
||||
finalMessage = packet as BackendMessage;
|
||||
}
|
||||
|
||||
const newUserMessageId =
|
||||
finalMessage?.parent_message || TEMP_USER_MESSAGE_ID;
|
||||
const newAssistantMessageId =
|
||||
finalMessage?.message_id || TEMP_ASSISTANT_MESSAGE_ID;
|
||||
updateFn([
|
||||
{
|
||||
tool_name: (packet as ToolCallMetadata).tool_name,
|
||||
tool_args: (packet as ToolCallMetadata).tool_args,
|
||||
tool_result: (packet as ToolCallMetadata).tool_result,
|
||||
messageId: newUserMessageId,
|
||||
message: currMessage,
|
||||
type: "user",
|
||||
files: currentMessageFiles,
|
||||
toolCalls: [],
|
||||
parentMessageId: parentMessage?.messageId || null,
|
||||
childrenMessageIds: [newAssistantMessageId],
|
||||
latestChildMessageId: newAssistantMessageId,
|
||||
},
|
||||
];
|
||||
} else if (Object.hasOwn(packet, "error")) {
|
||||
error = (packet as StreamingError).error;
|
||||
} else if (Object.hasOwn(packet, "message_id")) {
|
||||
finalMessage = packet as BackendMessage;
|
||||
{
|
||||
messageId: newAssistantMessageId,
|
||||
message: error || answer,
|
||||
type: error ? "error" : "assistant",
|
||||
retrievalType,
|
||||
query: finalMessage?.rephrased_query || query,
|
||||
documents:
|
||||
finalMessage?.context_docs?.top_documents || documents,
|
||||
citations: finalMessage?.citations || {},
|
||||
files: finalMessage?.files || aiMessageImages || [],
|
||||
toolCalls: finalMessage?.tool_calls || toolCalls,
|
||||
parentMessageId: newUserMessageId,
|
||||
},
|
||||
]);
|
||||
}
|
||||
if (isCancelledRef.current) {
|
||||
setIsCancelled(false);
|
||||
break;
|
||||
}
|
||||
}
|
||||
const updateFn = (messages: Message[]) => {
|
||||
const replacementsMap = finalMessage
|
||||
? new Map([
|
||||
[messages[0].messageId, TEMP_USER_MESSAGE_ID],
|
||||
[messages[1].messageId, TEMP_ASSISTANT_MESSAGE_ID],
|
||||
] as [number, number][])
|
||||
: null;
|
||||
upsertToCompleteMessageMap({
|
||||
messages: messages,
|
||||
replacementsMap: replacementsMap,
|
||||
completeMessageMapOverride: frozenCompleteMessageMap,
|
||||
});
|
||||
};
|
||||
const newUserMessageId =
|
||||
finalMessage?.parent_message || TEMP_USER_MESSAGE_ID;
|
||||
const newAssistantMessageId =
|
||||
finalMessage?.message_id || TEMP_ASSISTANT_MESSAGE_ID;
|
||||
updateFn([
|
||||
{
|
||||
messageId: newUserMessageId,
|
||||
message: currMessage,
|
||||
type: "user",
|
||||
files: currentMessageFiles,
|
||||
toolCalls: [],
|
||||
parentMessageId: parentMessage?.messageId || null,
|
||||
childrenMessageIds: [newAssistantMessageId],
|
||||
latestChildMessageId: newAssistantMessageId,
|
||||
},
|
||||
{
|
||||
messageId: newAssistantMessageId,
|
||||
message: error || answer,
|
||||
type: error ? "error" : "assistant",
|
||||
retrievalType,
|
||||
query: finalMessage?.rephrased_query || query,
|
||||
documents: finalMessage?.context_docs?.top_documents || documents,
|
||||
citations: finalMessage?.citations || {},
|
||||
files: finalMessage?.files || aiMessageImages || [],
|
||||
toolCalls: finalMessage?.tool_calls || toolCalls,
|
||||
parentMessageId: newUserMessageId,
|
||||
},
|
||||
]);
|
||||
if (isCancelledRef.current) {
|
||||
setIsCancelled(false);
|
||||
break;
|
||||
}
|
||||
}
|
||||
} catch (e: any) {
|
||||
@ -817,7 +951,6 @@ export function ChatPage({
|
||||
if (persona && persona.id !== livePersona.id) {
|
||||
// remove uploaded files
|
||||
setCurrentMessageFiles([]);
|
||||
|
||||
setSelectedPersona(persona);
|
||||
textAreaRef.current?.focus();
|
||||
router.push(buildChatUrl(searchParams, null, persona.id));
|
||||
@ -1051,7 +1184,7 @@ export function ChatPage({
|
||||
? completeMessageMap.get(message.parentMessageId)
|
||||
: null;
|
||||
return (
|
||||
<div key={i}>
|
||||
<div key={`${i}-${existingChatSessionId}`}>
|
||||
<HumanMessage
|
||||
content={message.message}
|
||||
files={message.files}
|
||||
@ -1107,103 +1240,116 @@ export function ChatPage({
|
||||
const previousMessage =
|
||||
i !== 0 ? messageHistory[i - 1] : null;
|
||||
return (
|
||||
<AIMessage
|
||||
key={message.messageId}
|
||||
messageId={message.messageId}
|
||||
content={message.message}
|
||||
files={message.files}
|
||||
query={messageHistory[i]?.query || undefined}
|
||||
personaName={livePersona.name}
|
||||
citedDocuments={getCitedDocumentsFromMessage(
|
||||
message
|
||||
)}
|
||||
toolCall={message?.toolCalls?.[0]}
|
||||
isComplete={
|
||||
i !== messageHistory.length - 1 ||
|
||||
!isStreaming
|
||||
<div
|
||||
key={`${i}-${existingChatSessionId}`}
|
||||
ref={
|
||||
i == messageHistory.length - 1
|
||||
? lastMessageRef
|
||||
: null
|
||||
}
|
||||
hasDocs={
|
||||
(message.documents &&
|
||||
message.documents.length > 0) === true
|
||||
}
|
||||
handleFeedback={
|
||||
i === messageHistory.length - 1 && isStreaming
|
||||
? undefined
|
||||
: (feedbackType) =>
|
||||
setCurrentFeedback([
|
||||
feedbackType,
|
||||
message.messageId as number,
|
||||
])
|
||||
}
|
||||
handleSearchQueryEdit={
|
||||
i === messageHistory.length - 1 &&
|
||||
!isStreaming
|
||||
? (newQuery) => {
|
||||
if (!previousMessage) {
|
||||
setPopup({
|
||||
type: "error",
|
||||
message:
|
||||
"Cannot edit query of first message - please refresh the page and try again.",
|
||||
});
|
||||
return;
|
||||
}
|
||||
>
|
||||
<AIMessage
|
||||
messageId={message.messageId}
|
||||
content={message.message}
|
||||
files={message.files}
|
||||
query={messageHistory[i]?.query || undefined}
|
||||
personaName={livePersona.name}
|
||||
citedDocuments={getCitedDocumentsFromMessage(
|
||||
message
|
||||
)}
|
||||
toolCall={
|
||||
message.toolCalls && message.toolCalls[0]
|
||||
}
|
||||
isComplete={
|
||||
i !== messageHistory.length - 1 ||
|
||||
!isStreaming
|
||||
}
|
||||
hasDocs={
|
||||
(message.documents &&
|
||||
message.documents.length > 0) === true
|
||||
}
|
||||
handleFeedback={
|
||||
i === messageHistory.length - 1 &&
|
||||
isStreaming
|
||||
? undefined
|
||||
: (feedbackType) =>
|
||||
setCurrentFeedback([
|
||||
feedbackType,
|
||||
message.messageId as number,
|
||||
])
|
||||
}
|
||||
handleSearchQueryEdit={
|
||||
i === messageHistory.length - 1 &&
|
||||
!isStreaming
|
||||
? (newQuery) => {
|
||||
if (!previousMessage) {
|
||||
setPopup({
|
||||
type: "error",
|
||||
message:
|
||||
"Cannot edit query of first message - please refresh the page and try again.",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
previousMessage.messageId === null
|
||||
) {
|
||||
setPopup({
|
||||
type: "error",
|
||||
message:
|
||||
"Cannot edit query of a pending message - please wait a few seconds and try again.",
|
||||
if (
|
||||
previousMessage.messageId === null
|
||||
) {
|
||||
setPopup({
|
||||
type: "error",
|
||||
message:
|
||||
"Cannot edit query of a pending message - please wait a few seconds and try again.",
|
||||
});
|
||||
return;
|
||||
}
|
||||
onSubmit({
|
||||
messageIdToResend:
|
||||
previousMessage.messageId,
|
||||
queryOverride: newQuery,
|
||||
});
|
||||
return;
|
||||
}
|
||||
onSubmit({
|
||||
messageIdToResend:
|
||||
previousMessage.messageId,
|
||||
queryOverride: newQuery,
|
||||
});
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
isCurrentlyShowingRetrieved={isShowingRetrieved}
|
||||
handleShowRetrieved={(messageNumber) => {
|
||||
if (isShowingRetrieved) {
|
||||
setSelectedMessageForDocDisplay(null);
|
||||
} else {
|
||||
if (messageNumber !== null) {
|
||||
setSelectedMessageForDocDisplay(
|
||||
messageNumber
|
||||
);
|
||||
: undefined
|
||||
}
|
||||
isCurrentlyShowingRetrieved={
|
||||
isShowingRetrieved
|
||||
}
|
||||
handleShowRetrieved={(messageNumber) => {
|
||||
if (isShowingRetrieved) {
|
||||
setSelectedMessageForDocDisplay(null);
|
||||
} else {
|
||||
setSelectedMessageForDocDisplay(-1);
|
||||
if (messageNumber !== null) {
|
||||
setSelectedMessageForDocDisplay(
|
||||
messageNumber
|
||||
);
|
||||
} else {
|
||||
setSelectedMessageForDocDisplay(-1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}}
|
||||
handleForceSearch={() => {
|
||||
if (
|
||||
previousMessage &&
|
||||
previousMessage.messageId
|
||||
) {
|
||||
onSubmit({
|
||||
messageIdToResend:
|
||||
previousMessage.messageId,
|
||||
forceSearch: true,
|
||||
});
|
||||
} else {
|
||||
setPopup({
|
||||
type: "error",
|
||||
message:
|
||||
"Failed to force search - please refresh the page and try again.",
|
||||
});
|
||||
}
|
||||
}}
|
||||
retrievalDisabled={retrievalDisabled}
|
||||
/>
|
||||
}}
|
||||
handleForceSearch={() => {
|
||||
if (
|
||||
previousMessage &&
|
||||
previousMessage.messageId
|
||||
) {
|
||||
onSubmit({
|
||||
messageIdToResend:
|
||||
previousMessage.messageId,
|
||||
forceSearch: true,
|
||||
});
|
||||
} else {
|
||||
setPopup({
|
||||
type: "error",
|
||||
message:
|
||||
"Failed to force search - please refresh the page and try again.",
|
||||
});
|
||||
}
|
||||
}}
|
||||
retrievalDisabled={retrievalDisabled}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<div key={i}>
|
||||
<div key={`${i}-${existingChatSessionId}`}>
|
||||
<AIMessage
|
||||
messageId={message.messageId}
|
||||
personaName={livePersona.name}
|
||||
@ -1222,7 +1368,9 @@ export function ChatPage({
|
||||
messageHistory.length > 0 &&
|
||||
messageHistory[messageHistory.length - 1].type ===
|
||||
"user" && (
|
||||
<div key={messageHistory.length}>
|
||||
<div
|
||||
key={`${messageHistory.length}-${existingChatSessionId}`}
|
||||
>
|
||||
<AIMessage
|
||||
messageId={null}
|
||||
personaName={livePersona.name}
|
||||
@ -1245,7 +1393,8 @@ export function ChatPage({
|
||||
)}
|
||||
|
||||
{/* Some padding at the bottom so the search bar has space at the bottom to not cover the last message*/}
|
||||
<div className={`min-h-[100px] w-full`}></div>
|
||||
<div ref={endPaddingRef} className=" h-[95px]" />
|
||||
<div ref={endDivRef}></div>
|
||||
|
||||
{livePersona &&
|
||||
livePersona.starter_messages &&
|
||||
@ -1255,22 +1404,25 @@ export function ChatPage({
|
||||
!isFetchingChatMessages && (
|
||||
<div
|
||||
className={`
|
||||
mx-auto
|
||||
px-4
|
||||
w-searchbar-xs
|
||||
2xl:w-searchbar-sm
|
||||
3xl:w-searchbar
|
||||
grid
|
||||
gap-4
|
||||
grid-cols-1
|
||||
grid-rows-1
|
||||
mt-4
|
||||
md:grid-cols-2
|
||||
mb-6`}
|
||||
mx-auto
|
||||
px-4
|
||||
w-searchbar-xs
|
||||
2xl:w-searchbar-sm
|
||||
3xl:w-searchbar
|
||||
grid
|
||||
gap-4
|
||||
grid-cols-1
|
||||
grid-rows-1
|
||||
mt-4
|
||||
md:grid-cols-2
|
||||
mb-6`}
|
||||
>
|
||||
{livePersona.starter_messages.map(
|
||||
(starterMessage, i) => (
|
||||
<div key={i} className="w-full">
|
||||
<div
|
||||
key={`${i}-${existingChatSessionId}`}
|
||||
className="w-full"
|
||||
>
|
||||
<StarterMessage
|
||||
starterMessage={starterMessage}
|
||||
onClick={() =>
|
||||
@ -1289,8 +1441,21 @@ export function ChatPage({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="absolute bottom-0 z-10 w-full">
|
||||
<div className="w-full pb-4">
|
||||
<div
|
||||
ref={inputRef}
|
||||
className="absolute bottom-0 z-10 w-full"
|
||||
>
|
||||
<div className="w-full relative pb-4">
|
||||
{aboveHorizon && (
|
||||
<div className="pointer-events-none w-full bg-transparent flex sticky justify-center">
|
||||
<button
|
||||
onClick={() => clientScrollToBottom(true)}
|
||||
className="p-1 pointer-events-auto rounded-2xl bg-background-strong border border-border mb-2 mx-auto "
|
||||
>
|
||||
<FiArrowDown size={18} />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
<ChatInputBar
|
||||
message={message}
|
||||
setMessage={setMessage}
|
||||
|
@ -5,7 +5,14 @@ import {
|
||||
} from "@/lib/search/interfaces";
|
||||
import { handleStream } from "@/lib/search/streamingUtils";
|
||||
import { FeedbackType } from "./types";
|
||||
import { RefObject } from "react";
|
||||
import {
|
||||
Dispatch,
|
||||
MutableRefObject,
|
||||
RefObject,
|
||||
SetStateAction,
|
||||
useEffect,
|
||||
useRef,
|
||||
} from "react";
|
||||
import {
|
||||
BackendMessage,
|
||||
ChatSession,
|
||||
@ -65,6 +72,14 @@ export async function createChatSession(
|
||||
return chatSessionResponseJson.chat_session_id;
|
||||
}
|
||||
|
||||
export type PacketType =
|
||||
| ToolCallMetadata
|
||||
| BackendMessage
|
||||
| AnswerPiecePacket
|
||||
| DocumentsResponse
|
||||
| ImageGenerationDisplay
|
||||
| StreamingError;
|
||||
|
||||
export async function* sendMessage({
|
||||
message,
|
||||
fileDescriptors,
|
||||
@ -150,14 +165,7 @@ export async function* sendMessage({
|
||||
throw Error(`Failed to send message - ${errorMsg}`);
|
||||
}
|
||||
|
||||
yield* handleStream<
|
||||
| AnswerPiecePacket
|
||||
| DocumentsResponse
|
||||
| BackendMessage
|
||||
| ImageGenerationDisplay
|
||||
| ToolCallMetadata
|
||||
| StreamingError
|
||||
>(sendMessageResponse);
|
||||
yield* handleStream<PacketType>(sendMessageResponse);
|
||||
}
|
||||
|
||||
export async function nameChatSession(chatSessionId: number, message: string) {
|
||||
@ -250,24 +258,6 @@ export async function* simulateLLMResponse(input: string, delay: number = 30) {
|
||||
}
|
||||
}
|
||||
|
||||
export function handleAutoScroll(
|
||||
endRef: RefObject<any>,
|
||||
scrollableRef: RefObject<any>,
|
||||
buffer: number = 300
|
||||
) {
|
||||
// Auto-scrolls if the user is within `buffer` of the bottom of the scrollableRef
|
||||
if (endRef && endRef.current && scrollableRef && scrollableRef.current) {
|
||||
if (
|
||||
scrollableRef.current.scrollHeight -
|
||||
scrollableRef.current.scrollTop -
|
||||
buffer <=
|
||||
scrollableRef.current.clientHeight
|
||||
) {
|
||||
endRef.current.scrollIntoView({ behavior: "smooth" });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function getHumanAndAIMessageFromMessageNumber(
|
||||
messageHistory: Message[],
|
||||
messageId: number
|
||||
@ -565,3 +555,92 @@ export async function uploadFilesForChat(
|
||||
|
||||
return [responseJson.files as FileDescriptor[], null];
|
||||
}
|
||||
|
||||
export async function useScrollonStream({
|
||||
isStreaming,
|
||||
scrollableDivRef,
|
||||
scrollDist,
|
||||
endDivRef,
|
||||
distance,
|
||||
debounce,
|
||||
}: {
|
||||
isStreaming: boolean;
|
||||
scrollableDivRef: RefObject<HTMLDivElement>;
|
||||
scrollDist: MutableRefObject<number>;
|
||||
endDivRef: RefObject<HTMLDivElement>;
|
||||
distance: number;
|
||||
debounce: number;
|
||||
}) {
|
||||
const preventScrollInterference = useRef<boolean>(false);
|
||||
const preventScroll = useRef<boolean>(false);
|
||||
const blockActionRef = useRef<boolean>(false);
|
||||
const previousScroll = useRef<number>(0);
|
||||
|
||||
useEffect(() => {
|
||||
if (isStreaming && scrollableDivRef && scrollableDivRef.current) {
|
||||
let newHeight: number = scrollableDivRef.current?.scrollTop!;
|
||||
const heightDifference = newHeight - previousScroll.current;
|
||||
previousScroll.current = newHeight;
|
||||
|
||||
// Prevent streaming scroll
|
||||
if (heightDifference < 0 && !preventScroll.current) {
|
||||
scrollableDivRef.current.style.scrollBehavior = "auto";
|
||||
scrollableDivRef.current.scrollTop = scrollableDivRef.current.scrollTop;
|
||||
scrollableDivRef.current.style.scrollBehavior = "smooth";
|
||||
preventScrollInterference.current = true;
|
||||
preventScroll.current = true;
|
||||
|
||||
setTimeout(() => {
|
||||
preventScrollInterference.current = false;
|
||||
}, 2000);
|
||||
setTimeout(() => {
|
||||
preventScroll.current = false;
|
||||
}, 10000);
|
||||
}
|
||||
|
||||
// Ensure can scroll if scroll down
|
||||
else if (!preventScrollInterference.current) {
|
||||
preventScroll.current = false;
|
||||
}
|
||||
if (
|
||||
scrollDist.current < distance &&
|
||||
!blockActionRef.current &&
|
||||
!blockActionRef.current &&
|
||||
!preventScroll.current &&
|
||||
endDivRef &&
|
||||
endDivRef.current
|
||||
) {
|
||||
// catch up if necessary!
|
||||
const scrollAmount = scrollDist.current + 10000;
|
||||
if (scrollDist.current > 140) {
|
||||
endDivRef.current.scrollIntoView();
|
||||
} else {
|
||||
blockActionRef.current = true;
|
||||
|
||||
scrollableDivRef?.current?.scrollBy({
|
||||
left: 0,
|
||||
top: Math.max(0, scrollAmount),
|
||||
behavior: "smooth",
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
blockActionRef.current = false;
|
||||
}, debounce);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// scroll on end of stream if within distance
|
||||
useEffect(() => {
|
||||
if (scrollableDivRef?.current && !isStreaming) {
|
||||
if (scrollDist.current < distance) {
|
||||
scrollableDivRef?.current?.scrollBy({
|
||||
left: 0,
|
||||
top: Math.max(scrollDist.current + 600, 0),
|
||||
behavior: "smooth",
|
||||
});
|
||||
}
|
||||
}
|
||||
}, [isStreaming]);
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user