Scroll to bottom working

This commit is contained in:
James Ketr 2025-04-02 17:35:10 -07:00
parent 847de136cf
commit 5dca368a87
2 changed files with 75 additions and 26 deletions

View File

@ -2,14 +2,6 @@ div {
box-sizing: border-box box-sizing: border-box
} }
.App {
display: flex;
text-align: center;
max-height: 100%;
height: 100%;
flex-direction: column;
}
.SystemInfo { .SystemInfo {
display: flex; display: flex;
flex-direction: column; flex-direction: column;

View File

@ -18,6 +18,7 @@ import AppBar from '@mui/material/AppBar';
import Drawer from '@mui/material/Drawer'; import Drawer from '@mui/material/Drawer';
import Toolbar from '@mui/material/Toolbar'; import Toolbar from '@mui/material/Toolbar';
import SettingsIcon from '@mui/icons-material/Settings'; import SettingsIcon from '@mui/icons-material/Settings';
import CloseIcon from '@mui/icons-material/Close';
import IconButton, { IconButtonProps } from '@mui/material/IconButton'; import IconButton, { IconButtonProps } from '@mui/material/IconButton';
import Box from '@mui/material/Box'; import Box from '@mui/material/Box';
import CssBaseline from '@mui/material/CssBaseline'; import CssBaseline from '@mui/material/CssBaseline';
@ -53,13 +54,10 @@ import '@fontsource/roboto/700.css';
//const use_mui_markdown = true //const use_mui_markdown = true
const use_mui_markdown = true const use_mui_markdown = true
const welcomeMarkdown = ` const welcomeMarkdown = `
# Welcome to Ketr-Chat # Welcome to Ketr-Chat
This LLM agent was built by James Ketrenos in order to provide answers to any questions you may have about his work history. This LLM agent was built by James Ketrenos in order to provide answers to any questions you may have about his work history. In addition to being a RAG enabled expert system, the LLM is configured with real-time access to weather, stocks, the current time, and can answer questions about the contents of a website.
In addition to being a RAG enabled expert system, the LLM is configured with real-time access to weather, stocks, the current time, and can answer questions about the contents of a website.
You can ask things like: You can ask things like:
* <ChatQuery text="What is James Ketrenos' work history?"/> * <ChatQuery text="What is James Ketrenos' work history?"/>
@ -559,13 +557,26 @@ const App = () => {
} }
}; };
// Scroll to bottom of conversation when conversation updates const isScrolledToBottom = useCallback(()=> {
useEffect(() => { // Current vertical scroll position
const queryElement = document.getElementById('QueryInput'); const scrollTop = window.scrollY || document.documentElement.scrollTop;
if (queryElement) {
queryElement.scrollIntoView(); // Total height of the page content
} const scrollHeight = document.documentElement.scrollHeight;
}, [conversation]);
// Height of the visible window
const clientHeight = document.documentElement.clientHeight;
// If we're at the bottom (allowing a small buffer of 5px)
return scrollTop + clientHeight >= scrollHeight - 5;
}, []);
const scrollToBottom = useCallback(() => {
console.log("Scroll to bottom");
window.scrollTo({
top: document.body.scrollHeight,
});
}, []);
// Set the snack pop-up and open it // Set the snack pop-up and open it
const setSnack = useCallback((message: string, severity: SeverityType = "success") => { const setSnack = useCallback((message: string, severity: SeverityType = "success") => {
@ -960,22 +971,25 @@ const App = () => {
const userMessage = [{ role: 'user', content: query }]; const userMessage = [{ role: 'user', content: query }];
let scrolledToBottom = isScrolledToBottom();
// Add user message to conversation // Add user message to conversation
const newConversation: MessageList = [ const newConversation: MessageList = [
...conversation, ...conversation,
...userMessage ...userMessage
]; ];
setConversation(newConversation); setConversation(newConversation);
scrollToBottom();
// Clear input // Clear input
setQuery(''); setQuery('');
setTimeout(() => { // setTimeout(() => {
document.getElementById("QueryIput")?.focus(); // document.getElementById("QueryIput")?.focus();
}, 1000); // }, 1000);
try { try {
scrolledToBottom = isScrolledToBottom();
setProcessing(true); setProcessing(true);
// Create a unique ID for the processing message // Create a unique ID for the processing message
const processingId = Date.now().toString(); const processingId = Date.now().toString();
@ -984,6 +998,9 @@ const App = () => {
...prev, ...prev,
{ role: 'assistant', content: 'Processing request...', id: processingId, isProcessing: true } { role: 'assistant', content: 'Processing request...', id: processingId, isProcessing: true }
]); ]);
if (scrolledToBottom) {
setTimeout(() => { scrollToBottom() }, 0);
}
// Make the fetch request with proper headers // Make the fetch request with proper headers
const response = await fetch(getConnectionBase(loc) + `/api/chat/${sessionId}`, { const response = await fetch(getConnectionBase(loc) + `/api/chat/${sessionId}`, {
@ -998,8 +1015,13 @@ const App = () => {
// We'll guess that the response will be around 500 tokens... // We'll guess that the response will be around 500 tokens...
const token_guess = 500; const token_guess = 500;
const estimate = Math.round(token_guess / lastEvalTPS + contextStatus.context_used / lastPromptTPS); const estimate = Math.round(token_guess / lastEvalTPS + contextStatus.context_used / lastPromptTPS);
scrolledToBottom = isScrolledToBottom();
setSnack(`Query sent. Response estimated in ${estimate}s.`, "info"); setSnack(`Query sent. Response estimated in ${estimate}s.`, "info");
startCountdown(Math.round(estimate)); startCountdown(Math.round(estimate));
if (scrolledToBottom) {
setTimeout(() => { scrollToBottom() }, 0);
}
if (!response.ok) { if (!response.ok) {
throw new Error(`Server responded with ${response.status}: ${response.statusText}`); throw new Error(`Server responded with ${response.status}: ${response.statusText}`);
@ -1034,18 +1056,23 @@ const App = () => {
// Force an immediate state update based on the message type // Force an immediate state update based on the message type
if (update.status === 'processing') { if (update.status === 'processing') {
scrolledToBottom = isScrolledToBottom();
// Update processing message with immediate re-render // Update processing message with immediate re-render
setConversation(prev => prev.map(msg => setConversation(prev => prev.map(msg =>
msg.id === processingId msg.id === processingId
? { ...msg, content: update.message } ? { ...msg, content: update.message }
: msg : msg
)); ));
if (scrolledToBottom) {
setTimeout(() => { scrollToBottom() }, 0);
}
// Add a small delay to ensure React has time to update the UI // Add a small delay to ensure React has time to update the UI
await new Promise(resolve => setTimeout(resolve, 0)); await new Promise(resolve => setTimeout(resolve, 0));
} else if (update.status === 'done') { } else if (update.status === 'done') {
// Replace processing message with final result // Replace processing message with final result
scrolledToBottom = isScrolledToBottom();
setConversation(prev => [ setConversation(prev => [
...prev.filter(msg => msg.id !== processingId), ...prev.filter(msg => msg.id !== processingId),
update.message update.message
@ -1056,12 +1083,19 @@ const App = () => {
setLastEvalTPS(evalTPS ? evalTPS : 35); setLastEvalTPS(evalTPS ? evalTPS : 35);
setLastPromptTPS(promptTPS ? promptTPS : 35); setLastPromptTPS(promptTPS ? promptTPS : 35);
updateContextStatus(); updateContextStatus();
if (scrolledToBottom) {
setTimeout(() => { scrollToBottom() }, 0);
}
} else if (update.status === 'error') { } else if (update.status === 'error') {
// Show error // Show error
scrolledToBottom = isScrolledToBottom();
setConversation(prev => [ setConversation(prev => [
...prev.filter(msg => msg.id !== processingId), ...prev.filter(msg => msg.id !== processingId),
{ role: 'assistant', type: 'error', content: update.message } { role: 'assistant', type: 'error', content: update.message }
]); ]);
if (scrolledToBottom) {
setTimeout(() => { scrollToBottom() }, 0);
}
} }
} catch (e) { } catch (e) {
setSnack("Error processing query", "error") setSnack("Error processing query", "error")
@ -1076,10 +1110,14 @@ const App = () => {
const update = JSON.parse(buffer); const update = JSON.parse(buffer);
if (update.status === 'done') { if (update.status === 'done') {
scrolledToBottom = isScrolledToBottom();
setConversation(prev => [ setConversation(prev => [
...prev.filter(msg => msg.id !== processingId), ...prev.filter(msg => msg.id !== processingId),
update.message update.message
]); ]);
if (scrolledToBottom) {
setTimeout(() => { scrollToBottom() }, 0);
}
} }
} catch (e) { } catch (e) {
setSnack("Error processing query", "error") setSnack("Error processing query", "error")
@ -1091,12 +1129,16 @@ const App = () => {
} catch (error) { } catch (error) {
console.error('Fetch error:', error); console.error('Fetch error:', error);
setSnack("Unable to process query", "error"); setSnack("Unable to process query", "error");
scrolledToBottom = isScrolledToBottom();
setConversation(prev => [ setConversation(prev => [
...prev.filter(msg => !msg.isProcessing), ...prev.filter(msg => !msg.isProcessing),
{ role: 'assistant', type: 'error', content: `Error: ${error}` } { role: 'assistant', type: 'error', content: `Error: ${error}` }
]); ]);
setProcessing(false); setProcessing(false);
stopCountdown(); stopCountdown();
if (scrolledToBottom) {
setTimeout(() => { scrollToBottom() }, 0);
}
} }
}; };
@ -1114,7 +1156,7 @@ const App = () => {
const Offset = styled('div')(({ theme }) => theme.mixins.toolbar); const Offset = styled('div')(({ theme }) => theme.mixins.toolbar);
return ( return (
<Box sx={{ display: 'flex', flexDirection: 'column', height: '100dvh' }}> <Box className="App" sx={{ display: 'flex', flexDirection: 'column', height: '100dvh' }}>
<CssBaseline /> <CssBaseline />
<AppBar <AppBar
position="fixed" position="fixed"
@ -1149,7 +1191,23 @@ const App = () => {
<Typography variant="h6" noWrap component="div"> <Typography variant="h6" noWrap component="div">
Ketr-Chat Ketr-Chat
</Typography> </Typography>
{
(mobileOpen === true || isScrolledToBottom()) &&
<Tooltip title="Close Settings">
<IconButton
color="inherit"
aria-label="close drawer"
edge="end"
onClick={handleDrawerToggle}
sx={{ mr: 2, right: 0, position: "absolute" }}
>
<CloseIcon />
</IconButton>
</Tooltip>
}
</Toolbar> </Toolbar>
</AppBar> </AppBar>
<Offset /> <Offset />
@ -1219,7 +1277,6 @@ const App = () => {
<TextField <TextField
variant="outlined" variant="outlined"
disabled={processing} disabled={processing}
autoFocus
fullWidth fullWidth
type="text" type="text"
value={query} value={query}