web: better overscroll behavior
This commit is contained in:
@@ -729,6 +729,8 @@ export default function App() {
|
|||||||
const wasSendingRef = useRef(false);
|
const wasSendingRef = useRef(false);
|
||||||
const pendingReplyScrollRef = useRef(false);
|
const pendingReplyScrollRef = useRef(false);
|
||||||
const transcriptTailSpacerHeightRef = useRef(TRANSCRIPT_BOTTOM_GAP);
|
const transcriptTailSpacerHeightRef = useRef(TRANSCRIPT_BOTTOM_GAP);
|
||||||
|
const transcriptTailSpacerSettleFrameRef = useRef<number | null>(null);
|
||||||
|
const transcriptViewKeyRef = useRef<string | null>(null);
|
||||||
const [contextMenu, setContextMenu] = useState<ContextMenuState | null>(null);
|
const [contextMenu, setContextMenu] = useState<ContextMenuState | null>(null);
|
||||||
const [isMobileSidebarOpen, setIsMobileSidebarOpen] = useState(false);
|
const [isMobileSidebarOpen, setIsMobileSidebarOpen] = useState(false);
|
||||||
const [sidebarQuery, setSidebarQuery] = useState("");
|
const [sidebarQuery, setSidebarQuery] = useState("");
|
||||||
@@ -753,6 +755,7 @@ export default function App() {
|
|||||||
const settleTranscriptTailSpacer = () => {
|
const settleTranscriptTailSpacer = () => {
|
||||||
const container = transcriptContainerRef.current;
|
const container = transcriptContainerRef.current;
|
||||||
const currentSpacerHeight = transcriptTailSpacerHeightRef.current;
|
const currentSpacerHeight = transcriptTailSpacerHeightRef.current;
|
||||||
|
if (currentSpacerHeight <= TRANSCRIPT_BOTTOM_GAP) return;
|
||||||
if (!container) {
|
if (!container) {
|
||||||
setTranscriptTailSpacer(TRANSCRIPT_BOTTOM_GAP);
|
setTranscriptTailSpacer(TRANSCRIPT_BOTTOM_GAP);
|
||||||
return;
|
return;
|
||||||
@@ -760,7 +763,22 @@ export default function App() {
|
|||||||
|
|
||||||
const scrollHeightWithoutSpacer = container.scrollHeight - currentSpacerHeight;
|
const scrollHeightWithoutSpacer = container.scrollHeight - currentSpacerHeight;
|
||||||
const requiredSpacerHeight = container.scrollTop + container.clientHeight - scrollHeightWithoutSpacer;
|
const requiredSpacerHeight = container.scrollTop + container.clientHeight - scrollHeightWithoutSpacer;
|
||||||
setTranscriptTailSpacer(requiredSpacerHeight);
|
setTranscriptTailSpacer(Math.min(currentSpacerHeight, requiredSpacerHeight));
|
||||||
|
};
|
||||||
|
|
||||||
|
const requestSettleTranscriptTailSpacer = () => {
|
||||||
|
if (transcriptTailSpacerHeightRef.current <= TRANSCRIPT_BOTTOM_GAP) return;
|
||||||
|
if (typeof window === "undefined") {
|
||||||
|
settleTranscriptTailSpacer();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (transcriptTailSpacerSettleFrameRef.current !== null) {
|
||||||
|
window.cancelAnimationFrame(transcriptTailSpacerSettleFrameRef.current);
|
||||||
|
}
|
||||||
|
transcriptTailSpacerSettleFrameRef.current = window.requestAnimationFrame(() => {
|
||||||
|
transcriptTailSpacerSettleFrameRef.current = null;
|
||||||
|
settleTranscriptTailSpacer();
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const focusComposer = () => {
|
const focusComposer = () => {
|
||||||
@@ -1032,6 +1050,7 @@ export default function App() {
|
|||||||
}, [quickPrompt, isQuickQuestionOpen]);
|
}, [quickPrompt, isQuickQuestionOpen]);
|
||||||
|
|
||||||
const selectedKey = selectedItem ? `${selectedItem.kind}:${selectedItem.id}` : null;
|
const selectedKey = selectedItem ? `${selectedItem.kind}:${selectedItem.id}` : null;
|
||||||
|
const transcriptViewKey = draftKind ? `draft:${draftKind}` : selectedKey ?? "empty";
|
||||||
const selectedChatPendingState = selectedItem?.kind === "chat" ? pendingChatStates[selectedItem.id] ?? null : null;
|
const selectedChatPendingState = selectedItem?.kind === "chat" ? pendingChatStates[selectedItem.id] ?? null : null;
|
||||||
const selectedSearchRunState = selectedItem?.kind === "search" ? runningSearchStates[selectedItem.id] ?? null : null;
|
const selectedSearchRunState = selectedItem?.kind === "search" ? runningSearchStates[selectedItem.id] ?? null : null;
|
||||||
const selectedChatIsActive = selectedItem?.kind === "chat" && (!!selectedChatPendingState || !!activeRuns.chats[selectedItem.id]);
|
const selectedChatIsActive = selectedItem?.kind === "chat" && (!!selectedChatPendingState || !!activeRuns.chats[selectedItem.id]);
|
||||||
@@ -1053,11 +1072,13 @@ export default function App() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
const didViewChange = transcriptViewKeyRef.current !== transcriptViewKey;
|
||||||
|
transcriptViewKeyRef.current = transcriptViewKey;
|
||||||
shouldAutoScrollRef.current = true;
|
shouldAutoScrollRef.current = true;
|
||||||
if (!isSendingActiveChat) {
|
if (didViewChange && !pendingReplyScrollRef.current) {
|
||||||
setTranscriptTailSpacer(TRANSCRIPT_BOTTOM_GAP);
|
setTranscriptTailSpacer(TRANSCRIPT_BOTTOM_GAP);
|
||||||
}
|
}
|
||||||
}, [isSendingActiveChat, selectedKey]);
|
}, [transcriptViewKey]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
selectedItemRef.current = selectedItem;
|
selectedItemRef.current = selectedItem;
|
||||||
@@ -1116,6 +1137,10 @@ export default function App() {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
return () => {
|
return () => {
|
||||||
|
if (transcriptTailSpacerSettleFrameRef.current !== null) {
|
||||||
|
window.cancelAnimationFrame(transcriptTailSpacerSettleFrameRef.current);
|
||||||
|
transcriptTailSpacerSettleFrameRef.current = null;
|
||||||
|
}
|
||||||
for (const controller of chatStreamAbortRefs.current.values()) {
|
for (const controller of chatStreamAbortRefs.current.values()) {
|
||||||
controller.abort();
|
controller.abort();
|
||||||
}
|
}
|
||||||
@@ -1144,6 +1169,16 @@ export default function App() {
|
|||||||
if (selectedChatPendingState) return selectedChatPendingState.messages.filter(isDisplayableMessage);
|
if (selectedChatPendingState) return selectedChatPendingState.messages.filter(isDisplayableMessage);
|
||||||
return messages.filter(isDisplayableMessage);
|
return messages.filter(isDisplayableMessage);
|
||||||
}, [messages, selectedChatPendingState]);
|
}, [messages, selectedChatPendingState]);
|
||||||
|
const displayMessagesLayoutKey = useMemo(
|
||||||
|
() => displayMessages.map((message) => `${message.id}:${message.content.length}`).join("|"),
|
||||||
|
[displayMessages]
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isSearchMode || isSendingActiveChat) return;
|
||||||
|
requestSettleTranscriptTailSpacer();
|
||||||
|
}, [displayMessagesLayoutKey, isSearchMode, isSendingActiveChat, selectedKey]);
|
||||||
|
|
||||||
const quickAnswerText = useMemo(() => {
|
const quickAnswerText = useMemo(() => {
|
||||||
for (let index = quickQuestionMessages.length - 1; index >= 0; index -= 1) {
|
for (let index = quickQuestionMessages.length - 1; index >= 0; index -= 1) {
|
||||||
const message = quickQuestionMessages[index];
|
const message = quickQuestionMessages[index];
|
||||||
@@ -1480,6 +1515,11 @@ export default function App() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleSendChat = async (content: string, attachments: ChatAttachment[]): Promise<SidebarSelection> => {
|
const handleSendChat = async (content: string, attachments: ChatAttachment[]): Promise<SidebarSelection> => {
|
||||||
|
const selectedModel = model.trim();
|
||||||
|
if (!selectedModel) {
|
||||||
|
throw new Error("No model available for selected provider");
|
||||||
|
}
|
||||||
|
|
||||||
pendingReplyScrollRef.current = true;
|
pendingReplyScrollRef.current = true;
|
||||||
expandTranscriptTailSpacer(getReplyScrollBufferHeight());
|
expandTranscriptTailSpacer(getReplyScrollBufferHeight());
|
||||||
|
|
||||||
@@ -1563,11 +1603,6 @@ export default function App() {
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const selectedModel = model.trim();
|
|
||||||
if (!selectedModel) {
|
|
||||||
throw new Error("No model available for selected provider");
|
|
||||||
}
|
|
||||||
|
|
||||||
const chatSummary = chats.find((chat) => chat.id === chatId);
|
const chatSummary = chats.find((chat) => chat.id === chatId);
|
||||||
const hasExistingTitle = Boolean(selectedChat?.id === chatId ? selectedChat.title?.trim() : chatSummary?.title?.trim());
|
const hasExistingTitle = Boolean(selectedChat?.id === chatId ? selectedChat.title?.trim() : chatSummary?.title?.trim());
|
||||||
if (!hasExistingTitle && !pendingTitleGenerationRef.current.has(chatId)) {
|
if (!hasExistingTitle && !pendingTitleGenerationRef.current.has(chatId)) {
|
||||||
@@ -1687,13 +1722,17 @@ export default function App() {
|
|||||||
if (currentSelection?.kind === "chat" && currentSelection.id === chatId) {
|
if (currentSelection?.kind === "chat" && currentSelection.id === chatId) {
|
||||||
await refreshChat(chatId);
|
await refreshChat(chatId);
|
||||||
}
|
}
|
||||||
settleTranscriptTailSpacer();
|
|
||||||
removePendingChatState(chatId);
|
removePendingChatState(chatId);
|
||||||
removeActiveRun("chat", chatId);
|
removeActiveRun("chat", chatId);
|
||||||
|
if (currentSelection?.kind === "chat" && currentSelection.id === chatId) {
|
||||||
|
requestSettleTranscriptTailSpacer();
|
||||||
|
}
|
||||||
return { kind: "chat", id: chatId };
|
return { kind: "chat", id: chatId };
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
removePendingChatState(chatId);
|
removePendingChatState(chatId);
|
||||||
removeActiveRun("chat", chatId);
|
removeActiveRun("chat", chatId);
|
||||||
|
pendingReplyScrollRef.current = false;
|
||||||
|
setTranscriptTailSpacer(TRANSCRIPT_BOTTOM_GAP);
|
||||||
throw err;
|
throw err;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -1954,6 +1993,9 @@ export default function App() {
|
|||||||
chatStreamAbortRefs.current.delete(chatId);
|
chatStreamAbortRefs.current.delete(chatId);
|
||||||
removePendingChatState(chatId);
|
removePendingChatState(chatId);
|
||||||
removeActiveRun("chat", chatId);
|
removeActiveRun("chat", chatId);
|
||||||
|
if (isCurrentSelection(target)) {
|
||||||
|
requestSettleTranscriptTailSpacer();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -2303,6 +2345,10 @@ export default function App() {
|
|||||||
sentTarget = await handleSendChat(content, attachments);
|
sentTarget = await handleSendChat(content, attachments);
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
if (!sentAsSearch) {
|
||||||
|
pendingReplyScrollRef.current = false;
|
||||||
|
setTranscriptTailSpacer(TRANSCRIPT_BOTTOM_GAP);
|
||||||
|
}
|
||||||
const message = err instanceof Error ? err.message : String(err);
|
const message = err instanceof Error ? err.message : String(err);
|
||||||
if (message.includes("bearer token")) {
|
if (message.includes("bearer token")) {
|
||||||
handleAuthFailure(message);
|
handleAuthFailure(message);
|
||||||
@@ -2564,6 +2610,9 @@ export default function App() {
|
|||||||
if (!container) return;
|
if (!container) return;
|
||||||
const distanceFromBottom = container.scrollHeight - container.scrollTop - container.clientHeight;
|
const distanceFromBottom = container.scrollHeight - container.scrollTop - container.clientHeight;
|
||||||
shouldAutoScrollRef.current = distanceFromBottom < 96;
|
shouldAutoScrollRef.current = distanceFromBottom < 96;
|
||||||
|
if (!isSearchMode && !isSendingActiveChat && distanceFromBottom > 0) {
|
||||||
|
settleTranscriptTailSpacer();
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{!isSearchMode ? (
|
{!isSearchMode ? (
|
||||||
|
|||||||
Reference in New Issue
Block a user