Scroll to bottom working
This commit is contained in:
parent
847de136cf
commit
5dca368a87
@ -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;
|
||||||
|
@ -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}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user