mirror of
https://github.com/danswer-ai/danswer.git
synced 2025-07-20 18:12:57 +02:00
Feature/scroll (#1694)
--------- Co-authored-by: “Pablo <“pablo@danswer.ai”>
This commit is contained in:
@ -26,9 +26,9 @@ import {
|
|||||||
getCitedDocumentsFromMessage,
|
getCitedDocumentsFromMessage,
|
||||||
getHumanAndAIMessageFromMessageNumber,
|
getHumanAndAIMessageFromMessageNumber,
|
||||||
getLastSuccessfulMessageId,
|
getLastSuccessfulMessageId,
|
||||||
handleAutoScroll,
|
|
||||||
handleChatFeedback,
|
handleChatFeedback,
|
||||||
nameChatSession,
|
nameChatSession,
|
||||||
|
PacketType,
|
||||||
personaIncludesRetrieval,
|
personaIncludesRetrieval,
|
||||||
processRawChatHistory,
|
processRawChatHistory,
|
||||||
removeMessage,
|
removeMessage,
|
||||||
@ -37,6 +37,7 @@ import {
|
|||||||
updateModelOverrideForChatSession,
|
updateModelOverrideForChatSession,
|
||||||
updateParentChildren,
|
updateParentChildren,
|
||||||
uploadFilesForChat,
|
uploadFilesForChat,
|
||||||
|
useScrollonStream,
|
||||||
} from "./lib";
|
} from "./lib";
|
||||||
import { useContext, useEffect, useRef, useState } from "react";
|
import { useContext, useEffect, useRef, useState } from "react";
|
||||||
import { usePopup } from "@/components/admin/connectors/Popup";
|
import { usePopup } from "@/components/admin/connectors/Popup";
|
||||||
@ -50,7 +51,7 @@ import { DanswerInitializingLoader } from "@/components/DanswerInitializingLoade
|
|||||||
import { FeedbackModal } from "./modal/FeedbackModal";
|
import { FeedbackModal } from "./modal/FeedbackModal";
|
||||||
import { ShareChatSessionModal } from "./modal/ShareChatSessionModal";
|
import { ShareChatSessionModal } from "./modal/ShareChatSessionModal";
|
||||||
import { ChatPersonaSelector } from "./ChatPersonaSelector";
|
import { ChatPersonaSelector } from "./ChatPersonaSelector";
|
||||||
import { FiShare2 } from "react-icons/fi";
|
import { FiArrowDown, FiShare2 } from "react-icons/fi";
|
||||||
import { ChatIntro } from "./ChatIntro";
|
import { ChatIntro } from "./ChatIntro";
|
||||||
import { AIMessage, HumanMessage } from "./message/Messages";
|
import { AIMessage, HumanMessage } from "./message/Messages";
|
||||||
import { ThreeDots } from "react-loader-spinner";
|
import { ThreeDots } from "react-loader-spinner";
|
||||||
@ -77,6 +78,7 @@ import { TbLayoutSidebarRightExpand } from "react-icons/tb";
|
|||||||
import { SIDEBAR_WIDTH_CONST } from "@/lib/constants";
|
import { SIDEBAR_WIDTH_CONST } from "@/lib/constants";
|
||||||
|
|
||||||
import ResizableSection from "@/components/resizable/ResizableSection";
|
import ResizableSection from "@/components/resizable/ResizableSection";
|
||||||
|
import { Button } from "@tremor/react";
|
||||||
|
|
||||||
const MAX_INPUT_HEIGHT = 200;
|
const MAX_INPUT_HEIGHT = 200;
|
||||||
const TEMP_USER_MESSAGE_ID = -1;
|
const TEMP_USER_MESSAGE_ID = -1;
|
||||||
@ -426,49 +428,111 @@ export function ChatPage({
|
|||||||
availableDocumentSets,
|
availableDocumentSets,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const [currentFeedback, setCurrentFeedback] = useState<
|
||||||
|
[FeedbackType, number] | null
|
||||||
|
>(null);
|
||||||
|
|
||||||
|
const [sharingModalVisible, setSharingModalVisible] =
|
||||||
|
useState<boolean>(false);
|
||||||
|
|
||||||
// state for cancelling streaming
|
// state for cancelling streaming
|
||||||
const [isCancelled, setIsCancelled] = useState(false);
|
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(() => {
|
useEffect(() => {
|
||||||
isCancelledRef.current = isCancelled;
|
isCancelledRef.current = isCancelled;
|
||||||
}, [isCancelled]);
|
}, [isCancelled]);
|
||||||
|
|
||||||
const [currentFeedback, setCurrentFeedback] = useState<
|
const distance = 500; // distance that should "engage" the scroll
|
||||||
[FeedbackType, number] | null
|
const debounce = 100; // time for debouncing
|
||||||
>(null);
|
|
||||||
const [sharingModalVisible, setSharingModalVisible] =
|
|
||||||
useState<boolean>(false);
|
|
||||||
|
|
||||||
// auto scroll as message comes out
|
useScrollonStream({
|
||||||
const scrollableDivRef = useRef<HTMLDivElement>(null);
|
isStreaming,
|
||||||
const endDivRef = useRef<HTMLDivElement>(null);
|
scrollableDivRef,
|
||||||
useEffect(() => {
|
scrollDist,
|
||||||
if (isStreaming || !message) {
|
endDivRef,
|
||||||
handleAutoScroll(endDivRef, scrollableDivRef);
|
distance,
|
||||||
}
|
debounce,
|
||||||
});
|
});
|
||||||
|
|
||||||
// scroll to bottom initially
|
|
||||||
const [hasPerformedInitialScroll, setHasPerformedInitialScroll] =
|
const [hasPerformedInitialScroll, setHasPerformedInitialScroll] =
|
||||||
useState(false);
|
useState(false);
|
||||||
|
|
||||||
|
// on new page
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
endDivRef.current?.scrollIntoView();
|
clientScrollToBottom();
|
||||||
setHasPerformedInitialScroll(true);
|
}, [chatSessionId]);
|
||||||
}, [isFetchingChatMessages]);
|
|
||||||
|
|
||||||
// handle re-sizing of the text area
|
// handle re-sizing of the text area
|
||||||
const textAreaRef = useRef<HTMLTextAreaElement>(null);
|
const textAreaRef = useRef<HTMLTextAreaElement>(null);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const textarea = textAreaRef.current;
|
handleInputResize();
|
||||||
if (textarea) {
|
|
||||||
textarea.style.height = "0px";
|
|
||||||
textarea.style.height = `${Math.min(
|
|
||||||
textarea.scrollHeight,
|
|
||||||
MAX_INPUT_HEIGHT
|
|
||||||
)}px`;
|
|
||||||
}
|
|
||||||
}, [message]);
|
}, [message]);
|
||||||
|
|
||||||
|
// tracks scrolling
|
||||||
|
useEffect(() => {
|
||||||
|
updateScrollTracking();
|
||||||
|
}, [messageHistory]);
|
||||||
|
|
||||||
// used for resizing of the document sidebar
|
// used for resizing of the document sidebar
|
||||||
const masterFlexboxRef = useRef<HTMLDivElement>(null);
|
const masterFlexboxRef = useRef<HTMLDivElement>(null);
|
||||||
const [maxDocumentSidebarWidth, setMaxDocumentSidebarWidth] = useState<
|
const [maxDocumentSidebarWidth, setMaxDocumentSidebarWidth] = useState<
|
||||||
@ -502,6 +566,53 @@ export function ChatPage({
|
|||||||
documentSidebarInitialWidth = Math.min(700, maxDocumentSidebarWidth);
|
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 ({
|
const onSubmit = async ({
|
||||||
messageIdToResend,
|
messageIdToResend,
|
||||||
messageOverride,
|
messageOverride,
|
||||||
@ -515,6 +626,7 @@ export function ChatPage({
|
|||||||
forceSearch?: boolean;
|
forceSearch?: boolean;
|
||||||
isSeededChat?: boolean;
|
isSeededChat?: boolean;
|
||||||
} = {}) => {
|
} = {}) => {
|
||||||
|
clientScrollToBottom();
|
||||||
let currChatSessionId: number;
|
let currChatSessionId: number;
|
||||||
let isNewSession = chatSessionId === null;
|
let isNewSession = chatSessionId === null;
|
||||||
const searchParamBasedChatSessionName =
|
const searchParamBasedChatSessionName =
|
||||||
@ -596,9 +708,7 @@ export function ChatPage({
|
|||||||
if (!parentMessage && frozenCompleteMessageMap.size === 2) {
|
if (!parentMessage && frozenCompleteMessageMap.size === 2) {
|
||||||
parentMessage = frozenCompleteMessageMap.get(SYSTEM_MESSAGE_ID) || null;
|
parentMessage = frozenCompleteMessageMap.get(SYSTEM_MESSAGE_ID) || null;
|
||||||
}
|
}
|
||||||
setMessage("");
|
resetInputBar();
|
||||||
setCurrentMessageFiles([]);
|
|
||||||
|
|
||||||
setIsStreaming(true);
|
setIsStreaming(true);
|
||||||
let answer = "";
|
let answer = "";
|
||||||
let query: string | null = null;
|
let query: string | null = null;
|
||||||
@ -614,7 +724,9 @@ export function ChatPage({
|
|||||||
try {
|
try {
|
||||||
const lastSuccessfulMessageId =
|
const lastSuccessfulMessageId =
|
||||||
getLastSuccessfulMessageId(currMessageHistory);
|
getLastSuccessfulMessageId(currMessageHistory);
|
||||||
for await (const packetBunch of sendMessage({
|
|
||||||
|
const stack = new CurrentMessageFIFO();
|
||||||
|
updateCurrentMessageFIFO(stack, {
|
||||||
message: currMessage,
|
message: currMessage,
|
||||||
fileDescriptors: currentMessageFiles,
|
fileDescriptors: currentMessageFiles,
|
||||||
parentMessageId: lastSuccessfulMessageId,
|
parentMessageId: lastSuccessfulMessageId,
|
||||||
@ -647,8 +759,32 @@ export function ChatPage({
|
|||||||
systemPromptOverride:
|
systemPromptOverride:
|
||||||
searchParams.get(SEARCH_PARAM_NAMES.SYSTEM_PROMPT) || undefined,
|
searchParams.get(SEARCH_PARAM_NAMES.SYSTEM_PROMPT) || undefined,
|
||||||
useExistingUserMessage: isSeededChat,
|
useExistingUserMessage: isSeededChat,
|
||||||
})) {
|
});
|
||||||
for (const packet of packetBunch) {
|
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")) {
|
if (Object.hasOwn(packet, "answer_piece")) {
|
||||||
answer += (packet as AnswerPiecePacket).answer_piece;
|
answer += (packet as AnswerPiecePacket).answer_piece;
|
||||||
} else if (Object.hasOwn(packet, "top_documents")) {
|
} else if (Object.hasOwn(packet, "top_documents")) {
|
||||||
@ -660,6 +796,14 @@ export function ChatPage({
|
|||||||
// we have to use -1)
|
// we have to use -1)
|
||||||
setSelectedMessageForDocDisplay(TEMP_USER_MESSAGE_ID);
|
setSelectedMessageForDocDisplay(TEMP_USER_MESSAGE_ID);
|
||||||
}
|
}
|
||||||
|
} 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")) {
|
} else if (Object.hasOwn(packet, "file_ids")) {
|
||||||
aiMessageImages = (packet as ImageGenerationDisplay).file_ids.map(
|
aiMessageImages = (packet as ImageGenerationDisplay).file_ids.map(
|
||||||
(fileId) => {
|
(fileId) => {
|
||||||
@ -682,20 +826,7 @@ export function ChatPage({
|
|||||||
} else if (Object.hasOwn(packet, "message_id")) {
|
} else if (Object.hasOwn(packet, "message_id")) {
|
||||||
finalMessage = packet as BackendMessage;
|
finalMessage = packet as BackendMessage;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
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 =
|
const newUserMessageId =
|
||||||
finalMessage?.parent_message || TEMP_USER_MESSAGE_ID;
|
finalMessage?.parent_message || TEMP_USER_MESSAGE_ID;
|
||||||
const newAssistantMessageId =
|
const newAssistantMessageId =
|
||||||
@ -717,18 +848,21 @@ export function ChatPage({
|
|||||||
type: error ? "error" : "assistant",
|
type: error ? "error" : "assistant",
|
||||||
retrievalType,
|
retrievalType,
|
||||||
query: finalMessage?.rephrased_query || query,
|
query: finalMessage?.rephrased_query || query,
|
||||||
documents: finalMessage?.context_docs?.top_documents || documents,
|
documents:
|
||||||
|
finalMessage?.context_docs?.top_documents || documents,
|
||||||
citations: finalMessage?.citations || {},
|
citations: finalMessage?.citations || {},
|
||||||
files: finalMessage?.files || aiMessageImages || [],
|
files: finalMessage?.files || aiMessageImages || [],
|
||||||
toolCalls: finalMessage?.tool_calls || toolCalls,
|
toolCalls: finalMessage?.tool_calls || toolCalls,
|
||||||
parentMessageId: newUserMessageId,
|
parentMessageId: newUserMessageId,
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
}
|
||||||
if (isCancelledRef.current) {
|
if (isCancelledRef.current) {
|
||||||
setIsCancelled(false);
|
setIsCancelled(false);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
const errorMsg = e.message;
|
const errorMsg = e.message;
|
||||||
upsertToCompleteMessageMap({
|
upsertToCompleteMessageMap({
|
||||||
@ -817,7 +951,6 @@ export function ChatPage({
|
|||||||
if (persona && persona.id !== livePersona.id) {
|
if (persona && persona.id !== livePersona.id) {
|
||||||
// remove uploaded files
|
// remove uploaded files
|
||||||
setCurrentMessageFiles([]);
|
setCurrentMessageFiles([]);
|
||||||
|
|
||||||
setSelectedPersona(persona);
|
setSelectedPersona(persona);
|
||||||
textAreaRef.current?.focus();
|
textAreaRef.current?.focus();
|
||||||
router.push(buildChatUrl(searchParams, null, persona.id));
|
router.push(buildChatUrl(searchParams, null, persona.id));
|
||||||
@ -1051,7 +1184,7 @@ export function ChatPage({
|
|||||||
? completeMessageMap.get(message.parentMessageId)
|
? completeMessageMap.get(message.parentMessageId)
|
||||||
: null;
|
: null;
|
||||||
return (
|
return (
|
||||||
<div key={i}>
|
<div key={`${i}-${existingChatSessionId}`}>
|
||||||
<HumanMessage
|
<HumanMessage
|
||||||
content={message.message}
|
content={message.message}
|
||||||
files={message.files}
|
files={message.files}
|
||||||
@ -1107,8 +1240,15 @@ export function ChatPage({
|
|||||||
const previousMessage =
|
const previousMessage =
|
||||||
i !== 0 ? messageHistory[i - 1] : null;
|
i !== 0 ? messageHistory[i - 1] : null;
|
||||||
return (
|
return (
|
||||||
|
<div
|
||||||
|
key={`${i}-${existingChatSessionId}`}
|
||||||
|
ref={
|
||||||
|
i == messageHistory.length - 1
|
||||||
|
? lastMessageRef
|
||||||
|
: null
|
||||||
|
}
|
||||||
|
>
|
||||||
<AIMessage
|
<AIMessage
|
||||||
key={message.messageId}
|
|
||||||
messageId={message.messageId}
|
messageId={message.messageId}
|
||||||
content={message.message}
|
content={message.message}
|
||||||
files={message.files}
|
files={message.files}
|
||||||
@ -1117,7 +1257,9 @@ export function ChatPage({
|
|||||||
citedDocuments={getCitedDocumentsFromMessage(
|
citedDocuments={getCitedDocumentsFromMessage(
|
||||||
message
|
message
|
||||||
)}
|
)}
|
||||||
toolCall={message?.toolCalls?.[0]}
|
toolCall={
|
||||||
|
message.toolCalls && message.toolCalls[0]
|
||||||
|
}
|
||||||
isComplete={
|
isComplete={
|
||||||
i !== messageHistory.length - 1 ||
|
i !== messageHistory.length - 1 ||
|
||||||
!isStreaming
|
!isStreaming
|
||||||
@ -1127,7 +1269,8 @@ export function ChatPage({
|
|||||||
message.documents.length > 0) === true
|
message.documents.length > 0) === true
|
||||||
}
|
}
|
||||||
handleFeedback={
|
handleFeedback={
|
||||||
i === messageHistory.length - 1 && isStreaming
|
i === messageHistory.length - 1 &&
|
||||||
|
isStreaming
|
||||||
? undefined
|
? undefined
|
||||||
: (feedbackType) =>
|
: (feedbackType) =>
|
||||||
setCurrentFeedback([
|
setCurrentFeedback([
|
||||||
@ -1166,7 +1309,9 @@ export function ChatPage({
|
|||||||
}
|
}
|
||||||
: undefined
|
: undefined
|
||||||
}
|
}
|
||||||
isCurrentlyShowingRetrieved={isShowingRetrieved}
|
isCurrentlyShowingRetrieved={
|
||||||
|
isShowingRetrieved
|
||||||
|
}
|
||||||
handleShowRetrieved={(messageNumber) => {
|
handleShowRetrieved={(messageNumber) => {
|
||||||
if (isShowingRetrieved) {
|
if (isShowingRetrieved) {
|
||||||
setSelectedMessageForDocDisplay(null);
|
setSelectedMessageForDocDisplay(null);
|
||||||
@ -1200,10 +1345,11 @@ export function ChatPage({
|
|||||||
}}
|
}}
|
||||||
retrievalDisabled={retrievalDisabled}
|
retrievalDisabled={retrievalDisabled}
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
return (
|
return (
|
||||||
<div key={i}>
|
<div key={`${i}-${existingChatSessionId}`}>
|
||||||
<AIMessage
|
<AIMessage
|
||||||
messageId={message.messageId}
|
messageId={message.messageId}
|
||||||
personaName={livePersona.name}
|
personaName={livePersona.name}
|
||||||
@ -1222,7 +1368,9 @@ export function ChatPage({
|
|||||||
messageHistory.length > 0 &&
|
messageHistory.length > 0 &&
|
||||||
messageHistory[messageHistory.length - 1].type ===
|
messageHistory[messageHistory.length - 1].type ===
|
||||||
"user" && (
|
"user" && (
|
||||||
<div key={messageHistory.length}>
|
<div
|
||||||
|
key={`${messageHistory.length}-${existingChatSessionId}`}
|
||||||
|
>
|
||||||
<AIMessage
|
<AIMessage
|
||||||
messageId={null}
|
messageId={null}
|
||||||
personaName={livePersona.name}
|
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*/}
|
{/* 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 &&
|
||||||
livePersona.starter_messages &&
|
livePersona.starter_messages &&
|
||||||
@ -1270,7 +1419,10 @@ export function ChatPage({
|
|||||||
>
|
>
|
||||||
{livePersona.starter_messages.map(
|
{livePersona.starter_messages.map(
|
||||||
(starterMessage, i) => (
|
(starterMessage, i) => (
|
||||||
<div key={i} className="w-full">
|
<div
|
||||||
|
key={`${i}-${existingChatSessionId}`}
|
||||||
|
className="w-full"
|
||||||
|
>
|
||||||
<StarterMessage
|
<StarterMessage
|
||||||
starterMessage={starterMessage}
|
starterMessage={starterMessage}
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
@ -1289,8 +1441,21 @@ export function ChatPage({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="absolute bottom-0 z-10 w-full">
|
<div
|
||||||
<div className="w-full pb-4">
|
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
|
<ChatInputBar
|
||||||
message={message}
|
message={message}
|
||||||
setMessage={setMessage}
|
setMessage={setMessage}
|
||||||
|
@ -5,7 +5,14 @@ import {
|
|||||||
} from "@/lib/search/interfaces";
|
} from "@/lib/search/interfaces";
|
||||||
import { handleStream } from "@/lib/search/streamingUtils";
|
import { handleStream } from "@/lib/search/streamingUtils";
|
||||||
import { FeedbackType } from "./types";
|
import { FeedbackType } from "./types";
|
||||||
import { RefObject } from "react";
|
import {
|
||||||
|
Dispatch,
|
||||||
|
MutableRefObject,
|
||||||
|
RefObject,
|
||||||
|
SetStateAction,
|
||||||
|
useEffect,
|
||||||
|
useRef,
|
||||||
|
} from "react";
|
||||||
import {
|
import {
|
||||||
BackendMessage,
|
BackendMessage,
|
||||||
ChatSession,
|
ChatSession,
|
||||||
@ -65,6 +72,14 @@ export async function createChatSession(
|
|||||||
return chatSessionResponseJson.chat_session_id;
|
return chatSessionResponseJson.chat_session_id;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type PacketType =
|
||||||
|
| ToolCallMetadata
|
||||||
|
| BackendMessage
|
||||||
|
| AnswerPiecePacket
|
||||||
|
| DocumentsResponse
|
||||||
|
| ImageGenerationDisplay
|
||||||
|
| StreamingError;
|
||||||
|
|
||||||
export async function* sendMessage({
|
export async function* sendMessage({
|
||||||
message,
|
message,
|
||||||
fileDescriptors,
|
fileDescriptors,
|
||||||
@ -150,14 +165,7 @@ export async function* sendMessage({
|
|||||||
throw Error(`Failed to send message - ${errorMsg}`);
|
throw Error(`Failed to send message - ${errorMsg}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
yield* handleStream<
|
yield* handleStream<PacketType>(sendMessageResponse);
|
||||||
| AnswerPiecePacket
|
|
||||||
| DocumentsResponse
|
|
||||||
| BackendMessage
|
|
||||||
| ImageGenerationDisplay
|
|
||||||
| ToolCallMetadata
|
|
||||||
| StreamingError
|
|
||||||
>(sendMessageResponse);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function nameChatSession(chatSessionId: number, message: string) {
|
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(
|
export function getHumanAndAIMessageFromMessageNumber(
|
||||||
messageHistory: Message[],
|
messageHistory: Message[],
|
||||||
messageId: number
|
messageId: number
|
||||||
@ -565,3 +555,92 @@ export async function uploadFilesForChat(
|
|||||||
|
|
||||||
return [responseJson.files as FileDescriptor[], null];
|
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]);
|
||||||
|
}
|
||||||
|
Reference in New Issue
Block a user