In Quick Start-Guide: Add an AI Chat Bot to a Vaadin + Spring Boot Application the code claims to feature “Sticky scroll: keep the latest answer in view.”
I am trying to implement the same in my AI chat agent, but it’s not working. I expect I have done something to interrupt the normal scroller behavior, but I can’t find the problem. Do you see the issue?
Please see the setupLayout method below.
@PageTitle("Console Whisperer Agent")
@Route("")
@Menu(order = 0, icon = LineAwesomeIconUrl.HOME_SOLID)
public class HomeViewAgent extends Composite<VerticalLayout> {
private static final Logger log = LoggerFactory.getLogger(HomeViewAgent.class);
private final MessageList messageList = new MessageList();
private final MessageInput messageInput = new MessageInput();
private final ChatMessageManager messageManager;
private final FileUploadHandler documentHandler;
private final StatusBarManager statusBarManager;
private final ProjectSetupCoordinator projectSetupCoordinator;
private final WebSocketLifecycleManager webSocketLifecycleManager;
private final ProjectService projectService;
private final MessageBroadcaster messageBroadcaster;
private final AgentAuthenticationService authService;
public HomeViewAgent(MessageBroadcaster messageBroadcaster, ProjectService projectService, AgentAuthenticationService authService) {
this.projectService = projectService;
this.messageBroadcaster = messageBroadcaster;
this.authService = authService;
messageManager = new ChatMessageManager(messageList);
documentHandler = new FileUploadHandler(docs -> {
if (!docs.isEmpty()) {
log.info("Documents attached: {}", docs.size());
}
});
statusBarManager = new StatusBarManager(projectService, this::handleProjectChange);
projectSetupCoordinator = new ProjectSetupCoordinator(projectService, messageManager);
webSocketLifecycleManager = new WebSocketLifecycleManager(messageBroadcaster, messageManager);
initializeComponents();
setupLayout();
checkAndSetupProject();
}
@Override
protected void onAttach(AttachEvent attachEvent) {
super.onAttach(attachEvent);
WebStorage.getItem(WebStorage.Storage.LOCAL_STORAGE, "jwt-token", token -> {
if (token != null && !token.isEmpty()) {
authService.setToken(token);
messageBroadcaster.attemptConnect();
log.info("Token found in session storage");
} else {
log.info("No token found, redirecting to login");
attachEvent.getUI().navigate(LoginViewAgent.class);
}
});
webSocketLifecycleManager.handleAttach(attachEvent);
addPageUnloadDetection();
}
@Override
protected void onDetach(DetachEvent detachEvent) {
super.onDetach(detachEvent);
webSocketLifecycleManager.handleDetach();
}
private void checkAndSetupProject() {
projectSetupCoordinator.checkAndSetupProject(statusBarManager::updateStatusBar);
}
private void setupLayout() {
final Scroller scroller = new Scroller(messageList);
scroller.setHeightFull();
getContent().addAndExpand(scroller);
VerticalLayout inputArea = new VerticalLayout();
inputArea.setSpacing(false);
inputArea.add(documentHandler.getUploadComponent(), messageInput);
getContent().add(statusBarManager.getStatusBar(), inputArea);
messageInput.focus();
}
private void initializeComponents() {
messageInput.setId("message-input");
messageList.setId("message-list");
messageList.setMarkdown(true);
messageInput.setWidthFull();
messageInput.addSubmitListener(this::handleMessageSubmission);
}
private void handleMessageSubmission(MessageInput.SubmitEvent event) {
String text = event.getValue();
String showBibleContent = readShowBible();
if (showBibleContent == null) {
return;
}
String displayMessage = documentHandler.formatMessageWithDocumentInfo(text);
messageManager.appendMessage(displayMessage, "You");
UserMessage userMessage = new UserMessage(text, showBibleContent, documentHandler.createDocumentInfoList(), ToolMode.API_CALLS);
messageBroadcaster.sendMessage(userMessage);
documentHandler.clearDocuments();
messageManager.prepareForServerResponse();
}
private String readShowBible() {
try {
return projectService.readShowBible();
} catch (IOException e) {
log.error("Failed to read show bible", e);
Notification.show("Warning: Could not read show bible file", 3000,
Notification.Position.BOTTOM_CENTER).addThemeVariants(NotificationVariant.LUMO_WARNING);
return "";
} catch (IllegalStateException e) {
log.error("No project configured", e);
Notification.show("Please select a project first", 3000,
Notification.Position.BOTTOM_CENTER).addThemeVariants(NotificationVariant.LUMO_ERROR);
checkAndSetupProject();
return null;
}
}
private void handleProjectChange() {
statusBarManager.updateStatusBar();
projectSetupCoordinator.resetProject();
}
private void addPageUnloadDetection() {
getUI().ifPresent(ui -> ui.getPage().executeJs("""
window.addEventListener('beforeunload', function(e) {
// Mark shutdown intent but DON'T clear the token
window.sessionStorage.setItem('shutdown-intent', Date.now().toString());
if (navigator.sendBeacon) {
navigator.sendBeacon('/api/shutdown', '');
} else {
fetch('/api/shutdown', {
method: 'POST',
keepalive: true
}).catch(() => {});
}
});
// Clear any stale shutdown intent on successful page load
window.sessionStorage.removeItem('shutdown-intent');
"""));
}
}
Please see appendMessage below.
public class ChatMessageManager {
private final MessageList messageList;
private final AtomicReference<MessageListItem> lastServerMessage = new AtomicReference<>();
public ChatMessageManager(MessageList messageList) {
this.messageList = messageList;
}
public void appendMessage(String text, String sender) {
MessageListItem item = new MessageListItem(text, Instant.now(), sender);
item.setUserColorIndex(0);
messageList.addItem(item);
lastServerMessage.set(null);
}
public void appendStreamingMessage(String message) {
MessageListItem currentMessage = lastServerMessage.get();
if (currentMessage != null) {
currentMessage.setText(currentMessage.getText() + message);
} else {
appendMessage(message, "Server");
}
}
public void prepareForServerResponse() {
MessageListItem serverResponse = new MessageListItem("", Instant.now(), "Server");
serverResponse.setUserColorIndex(1);
messageList.addItem(serverResponse);
lastServerMessage.set(serverResponse);
}
public void clearMessages() {
messageList.setItems();
lastServerMessage.set(null);
}
}
Messages overflow off the bottom are never scrolled into view.