diff --git a/build.gradle b/build.gradle index 7823b36..54d99d4 100644 --- a/build.gradle +++ b/build.gradle @@ -1,9 +1,9 @@ plugins { id 'java' - id 'com.vaadin' version '24.5.7' + id 'com.vaadin' version '24.5.8' id 'org.cyclonedx.bom' version '1.10.0' id 'com.gorylenko.gradle-git-properties' version '2.4.2' - id 'org.springframework.boot' version '3.3.5' + id 'org.springframework.boot' version '3.4.0' id 'io.spring.dependency-management' version '1.1.6' id 'org.eclipse.jkube.kubernetes' version '1.17.0' } @@ -21,15 +21,17 @@ repositories { } ext { - set('springCloudVersion', "2023.0.3") - set('vaadinVersion', "24.5.4") + set('springCloudVersion', "2024.0.0") + set('vaadinVersion', "24.5.8") } dependencies { implementation 'org.springframework.boot:spring-boot-starter-actuator' implementation 'org.springframework.boot:spring-boot-starter-web' implementation 'org.springframework.cloud:spring-cloud-starter-openfeign' + implementation 'org.apache.commons:commons-collections4:4.4' implementation 'io.github.openfeign:feign-hc5:13.5' + implementation 'com.playtika.reactivefeign:feign-reactor-spring-cloud-starter:4.2.1' implementation 'com.vaadin:vaadin-spring-boot-starter' implementation 'org.apache.commons:commons-lang3' implementation 'org.commonmark:commonmark:0.24.0' diff --git a/docs/BUILD.md b/docs/BUILD.md index 45829a6..312dc56 100644 --- a/docs/BUILD.md +++ b/docs/BUILD.md @@ -3,6 +3,8 @@ ## How to Build ```bash +npm install @vaadin/hilla-lit-form +npm install @vaadin/hilla-react-signals ./gradlew clean build ``` diff --git a/src/main/frontend/flow/markdown-component.tsx b/src/main/frontend/flow/markdown-component.tsx new file mode 100644 index 0000000..e3491d8 --- /dev/null +++ b/src/main/frontend/flow/markdown-component.tsx @@ -0,0 +1,22 @@ +import React, {useEffect, useState} from 'react'; +import {ReactAdapterElement} from 'Frontend/generated/flow/ReactAdapter'; +import {effect, signal} from "@vaadin/hilla-react-signals"; +import Markdown from "react-markdown"; + +class MarkdownElement extends ReactAdapterElement { + + markdown = signal(''); + + protected override render() { + // In a React component, we could use the signal value directly, + // but it doesn't trigger an update in the ReactAdapterElement render method. + // Instead, pass the signal value to useState for React. + const [content, setContent] = useState(''); + useEffect(() => effect(() => { + setContent(this.markdown.value); + }), []); + return {content}; + } +} + +customElements.define('markdown-component', MarkdownElement); \ No newline at end of file diff --git a/src/main/java/org/cftoolsuite/RobertUiApplication.java b/src/main/java/org/cftoolsuite/RobertUiApplication.java index d38f224..4a1fedf 100644 --- a/src/main/java/org/cftoolsuite/RobertUiApplication.java +++ b/src/main/java/org/cftoolsuite/RobertUiApplication.java @@ -9,9 +9,11 @@ import com.vaadin.flow.server.PWA; import com.vaadin.flow.theme.Theme; import com.vaadin.flow.theme.lumo.Lumo; +import reactivefeign.spring.config.EnableReactiveFeignClients; @SpringBootApplication @EnableFeignClients +@EnableReactiveFeignClients @ConfigurationPropertiesScan @Theme(themeClass = Lumo.class, variant = Lumo.DARK) @PWA(name = "A simple user interface to interact with a robert instance", shortName = "robert-ui") diff --git a/src/main/java/org/cftoolsuite/client/RefactorClient.java b/src/main/java/org/cftoolsuite/client/RefactorClient.java index 6434432..64540ca 100644 --- a/src/main/java/org/cftoolsuite/client/RefactorClient.java +++ b/src/main/java/org/cftoolsuite/client/RefactorClient.java @@ -1,11 +1,13 @@ package org.cftoolsuite.client; +import java.util.Map; import java.util.Set; import org.cftoolsuite.domain.GitRequest; import org.cftoolsuite.domain.GitResponse; import org.cftoolsuite.domain.IngestRequest; import org.cftoolsuite.domain.LanguageExtensions; +import org.cftoolsuite.domain.chat.Inquiry; import org.springframework.cloud.openfeign.FeignClient; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; @@ -20,8 +22,8 @@ public interface RefactorClient { @PostMapping(value = "/ingest") ResponseEntity ingest(@RequestBody IngestRequest request); - @GetMapping("/chat") - ResponseEntity chat(@RequestParam("q") String message); + @PostMapping("/api/chat") + public ResponseEntity chat(@RequestBody Inquiry inquiry); @PostMapping(value = "/refactor") ResponseEntity refactor(@RequestBody GitRequest request); diff --git a/src/main/java/org/cftoolsuite/client/RefactorStreamingClient.java b/src/main/java/org/cftoolsuite/client/RefactorStreamingClient.java new file mode 100644 index 0000000..7323d61 --- /dev/null +++ b/src/main/java/org/cftoolsuite/client/RefactorStreamingClient.java @@ -0,0 +1,15 @@ +package org.cftoolsuite.client; + +import org.cftoolsuite.domain.chat.Inquiry; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import reactivefeign.spring.config.ReactiveFeignClient; +import reactor.core.publisher.Flux; + +@ReactiveFeignClient(name="refactor-streaming-service", url="${refactor.service.url}") +public interface RefactorStreamingClient { + + @PostMapping("/api/stream/chat") + public Flux streamResponseToQuestion(@RequestBody Inquiry inquiry); +} + diff --git a/src/main/java/org/cftoolsuite/config/OpenFeign.java b/src/main/java/org/cftoolsuite/config/OpenFeign.java deleted file mode 100644 index 4b04d8f..0000000 --- a/src/main/java/org/cftoolsuite/config/OpenFeign.java +++ /dev/null @@ -1,39 +0,0 @@ -package org.cftoolsuite.config; - -import org.apache.hc.client5.http.impl.classic.CloseableHttpClient; -import org.apache.hc.client5.http.impl.classic.HttpClients; -import org.springframework.beans.factory.ObjectFactory; -import org.springframework.boot.autoconfigure.http.HttpMessageConverters; -import org.springframework.cloud.openfeign.support.ResponseEntityDecoder; -import org.springframework.cloud.openfeign.support.SpringDecoder; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; - -import feign.Logger; -import feign.codec.Decoder; - -@Configuration -// @see https://howtodoinjava.com/spring-cloud/spring-boot-openfeign-client-tutorial/ -public class OpenFeign { - - @Bean - public CloseableHttpClient feignClient() { - return HttpClients.createDefault(); - } - - @Bean - Logger.Level feignLoggerLevel() { - return Logger.Level.FULL; - } - - @Bean - public Decoder feignDecoder() { - return new ResponseEntityDecoder(new SpringDecoder(messageConverters())); - } - - private ObjectFactory messageConverters() { - final HttpMessageConverters httpMessageConverters = new HttpMessageConverters(new MappingJackson2HttpMessageConverter()); - return () -> httpMessageConverters; - } -} diff --git a/src/main/java/org/cftoolsuite/domain/chat/FilterMetadata.java b/src/main/java/org/cftoolsuite/domain/chat/FilterMetadata.java new file mode 100644 index 0000000..9d359b5 --- /dev/null +++ b/src/main/java/org/cftoolsuite/domain/chat/FilterMetadata.java @@ -0,0 +1,4 @@ +package org.cftoolsuite.domain.chat; + +public record FilterMetadata(String key, Object value) { +} diff --git a/src/main/java/org/cftoolsuite/domain/chat/Inquiry.java b/src/main/java/org/cftoolsuite/domain/chat/Inquiry.java new file mode 100644 index 0000000..aaa987d --- /dev/null +++ b/src/main/java/org/cftoolsuite/domain/chat/Inquiry.java @@ -0,0 +1,6 @@ +package org.cftoolsuite.domain.chat; + +import java.util.List; + +public record Inquiry(String question, List filter) { +} diff --git a/src/main/java/org/cftoolsuite/ui/MainLayout.java b/src/main/java/org/cftoolsuite/ui/MainLayout.java index d7badc3..21634f2 100644 --- a/src/main/java/org/cftoolsuite/ui/MainLayout.java +++ b/src/main/java/org/cftoolsuite/ui/MainLayout.java @@ -1,5 +1,7 @@ package org.cftoolsuite.ui; +import com.vaadin.flow.component.html.Image; +import com.vaadin.flow.server.StreamResource; import org.cftoolsuite.client.ModeClient; import org.cftoolsuite.ui.view.ChatView; import org.cftoolsuite.ui.view.HomeView; @@ -8,10 +10,8 @@ import org.cftoolsuite.ui.view.SearchView; import com.vaadin.flow.component.Component; -import com.vaadin.flow.component.accordion.Accordion; import com.vaadin.flow.component.applayout.AppLayout; import com.vaadin.flow.component.applayout.DrawerToggle; -import com.vaadin.flow.component.details.DetailsVariant; import com.vaadin.flow.component.html.Div; import com.vaadin.flow.component.html.Span; import com.vaadin.flow.component.icon.Icon; @@ -28,9 +28,6 @@ public class MainLayout extends AppLayout { public MainLayout(ModeClient modeClient) { Tab homeTab = createTab(VaadinIcon.HOME.create(), "Home", HomeView.class); - Accordion accordion = new Accordion(); - accordion.setSizeFull(); - Tabs actionTabs = createTabs(); if (modeClient.isAdvancedModeConfigured()) { Tab ingestTab = createTab(VaadinIcon.PLUG.create(),"Ingest", IngestView.class); @@ -42,10 +39,9 @@ public MainLayout(ModeClient modeClient) { } Tab refactorTab = createTab(VaadinIcon.PLAY.create(), "Refactor", RefactorView.class); actionTabs.add(refactorTab); - accordion.add("Actions", actionTabs).addThemeVariants(DetailsVariant.REVERSE); addToNavbar(true, homeTab, new DrawerToggle()); - addToDrawer(accordion); + addToDrawer(getLogoImage(), actionTabs); } private Tabs createTabs() { @@ -72,4 +68,12 @@ private Tab createTab(Icon icon, String label, Class layout return tab; } + private Image getLogoImage() { + StreamResource imageResource = new StreamResource("robert.png", + () -> getClass().getResourceAsStream("/static/robert.png")); + Image logo = new Image(imageResource, "Logo"); + logo.setWidth("240px"); + return logo; + } + } diff --git a/src/main/java/org/cftoolsuite/ui/component/Markdown.java b/src/main/java/org/cftoolsuite/ui/component/Markdown.java index 244011b..2cc0497 100644 --- a/src/main/java/org/cftoolsuite/ui/component/Markdown.java +++ b/src/main/java/org/cftoolsuite/ui/component/Markdown.java @@ -1,14 +1,16 @@ package org.cftoolsuite.ui.component; -import com.vaadin.flow.component.Composite; -import com.vaadin.flow.component.html.Div; -import org.commonmark.parser.Parser; -import org.commonmark.renderer.html.HtmlRenderer; +import com.vaadin.flow.component.Tag; +import com.vaadin.flow.component.dependency.JsModule; +import com.vaadin.flow.component.dependency.NpmPackage; +import com.vaadin.flow.component.react.ReactAdapterComponent; -public class Markdown extends Composite
{ +@NpmPackage(value = "react-markdown", version = "9.0.1") +@JsModule("./flow/markdown-component.tsx") +@Tag("markdown-component") +public class Markdown extends ReactAdapterComponent { - Parser parser = Parser.builder().build(); - HtmlRenderer renderer = HtmlRenderer.builder().build(); + private String markdown = ""; public Markdown() { } @@ -18,8 +20,20 @@ public Markdown(String markdown) { } public void setMarkdown(String markdown) { - getContent().getElement().setProperty("innerHTML", - renderer.render(parser.parse(markdown)) - ); + this.markdown = markdown; + getElement().executeJs("this.markdown.value = $0", markdown); } -} + + public void appendMarkdown(String additionalMarkdown) { + this.markdown += additionalMarkdown; + getElement().executeJs("this.markdown.value += $0", additionalMarkdown); + } + + public void clear() { + setMarkdown(""); + } + + public String getMarkdown() { + return markdown; + } +} \ No newline at end of file diff --git a/src/main/java/org/cftoolsuite/ui/component/MetadataFilter.java b/src/main/java/org/cftoolsuite/ui/component/MetadataFilter.java new file mode 100644 index 0000000..591033c --- /dev/null +++ b/src/main/java/org/cftoolsuite/ui/component/MetadataFilter.java @@ -0,0 +1,112 @@ +package org.cftoolsuite.ui.component; + +import com.vaadin.flow.component.button.Button; +import com.vaadin.flow.component.customfield.CustomField; +import com.vaadin.flow.component.grid.Grid; +import com.vaadin.flow.component.icon.VaadinIcon; +import com.vaadin.flow.component.orderedlayout.FlexComponent; +import com.vaadin.flow.component.orderedlayout.HorizontalLayout; +import com.vaadin.flow.component.orderedlayout.VerticalLayout; +import com.vaadin.flow.component.textfield.TextField; +import org.cftoolsuite.domain.chat.FilterMetadata; + +import java.util.ArrayList; +import java.util.List; + +public class MetadataFilter extends CustomField> { + private final Grid grid; + private final List entries; + private final TextField keyField; + private final TextField valueField; + private final Button addButton; + + public MetadataFilter() { + entries = new ArrayList<>(); + grid = new Grid<>(); + + grid.addColumn(MetadataEntry::getKey).setHeader("Key").setFlexGrow(1); + grid.addColumn(MetadataEntry::getValue).setHeader("Value").setFlexGrow(1); + grid.addComponentColumn(this::createRemoveButton).setWidth("100px").setFlexGrow(0); + + grid.setItems(entries); + + keyField = new TextField("Key"); + valueField = new TextField("Value"); + addButton = new Button("Add", VaadinIcon.PLUS.create()); + addButton.addClickListener(e -> addEntry()); + + HorizontalLayout inputLayout = new HorizontalLayout(keyField, valueField, addButton); + inputLayout.setWidth("100%"); + inputLayout.setAlignItems(FlexComponent.Alignment.END); + + VerticalLayout layout = new VerticalLayout(inputLayout, grid); + layout.setSpacing(false); + layout.setPadding(false); + add(layout); + } + + private Button createRemoveButton(MetadataEntry entry) { + Button removeButton = new Button(VaadinIcon.TRASH.create()); + removeButton.addClickListener(e -> { + entries.remove(entry); + grid.getDataProvider().refreshAll(); + updateValue(); + }); + return removeButton; + } + + private void addEntry() { + String key = keyField.getValue().trim(); + String value = valueField.getValue().trim(); + + if (!key.isEmpty() && !value.isEmpty()) { + entries.add(new MetadataEntry(key, value)); + keyField.clear(); + valueField.clear(); + grid.getDataProvider().refreshAll(); + updateValue(); + } + } + + @Override + protected List generateModelValue() { + if (entries.isEmpty()) { + return null; + } + + List metadata = new ArrayList<>(); + for (MetadataEntry entry : entries) { + metadata.add(new FilterMetadata(entry.getKey(), entry.getValue())); + } + return metadata; + } + + @Override + public void setPresentationValue(List metadata) { + entries.clear(); + if (metadata != null) { + metadata.forEach(fm -> + entries.add(new MetadataEntry(fm.key(), fm.value())) + ); + } + grid.getDataProvider().refreshAll(); + } + + private static class MetadataEntry { + private final String key; + private final Object value; + + public MetadataEntry(String key, Object value) { + this.key = key; + this.value = value; + } + + public String getKey() { + return key; + } + + public Object getValue() { + return value; + } + } +} diff --git a/src/main/java/org/cftoolsuite/ui/view/BaseStreamingView.java b/src/main/java/org/cftoolsuite/ui/view/BaseStreamingView.java new file mode 100644 index 0000000..9d79bb1 --- /dev/null +++ b/src/main/java/org/cftoolsuite/ui/view/BaseStreamingView.java @@ -0,0 +1,53 @@ +package org.cftoolsuite.ui.view; + +import com.vaadin.flow.component.Key; +import com.vaadin.flow.component.UI; +import com.vaadin.flow.component.html.Div; +import com.vaadin.flow.component.notification.Notification; +import com.vaadin.flow.component.notification.Notification.Position; +import com.vaadin.flow.component.notification.NotificationVariant; +import com.vaadin.flow.component.orderedlayout.VerticalLayout; +import org.cftoolsuite.client.ModeClient; +import org.cftoolsuite.client.RefactorStreamingClient; + +public abstract class BaseStreamingView extends VerticalLayout { + + protected RefactorStreamingClient refactorStreamingClient; + protected ModeClient modeClient; + + public BaseStreamingView(RefactorStreamingClient refactorStreamingClient, ModeClient modeClient) { + this.refactorStreamingClient = refactorStreamingClient; + this.modeClient = modeClient; + setupUI(); + } + + protected abstract void setupUI(); + + protected abstract void clearAllFields(); + + protected void showNotification(String message, NotificationVariant variant) { + Notification notification = new Notification(message, 5000, Position.TOP_STRETCH); + notification.setPosition(Position.TOP_CENTER); + notification.addThemeVariants(variant); + + Div content = new Div(); + content.setText(message); + content.getStyle().set("cursor", "pointer"); + content.addClickListener(event -> notification.close()); + + notification.add(content); + + UI.getCurrent().addShortcutListener( + notification::close, + Key.ESCAPE + ); + + notification.open(); + + notification.addDetachListener(event -> + UI.getCurrent().getPage().executeJs( + "window.Vaadin.Flow.notificationEscListener.remove()" + ) + ); + } +} \ No newline at end of file diff --git a/src/main/java/org/cftoolsuite/ui/view/BaseView.java b/src/main/java/org/cftoolsuite/ui/view/BaseView.java index 98c14d3..d6ecb1b 100644 --- a/src/main/java/org/cftoolsuite/ui/view/BaseView.java +++ b/src/main/java/org/cftoolsuite/ui/view/BaseView.java @@ -10,12 +10,10 @@ import com.vaadin.flow.component.Key; import com.vaadin.flow.component.UI; import com.vaadin.flow.component.html.Div; -import com.vaadin.flow.component.html.Image; import com.vaadin.flow.component.notification.Notification; import com.vaadin.flow.component.notification.Notification.Position; import com.vaadin.flow.component.notification.NotificationVariant; import com.vaadin.flow.component.orderedlayout.VerticalLayout; -import com.vaadin.flow.server.StreamResource; public abstract class BaseView extends VerticalLayout { @@ -25,11 +23,6 @@ public abstract class BaseView extends VerticalLayout { public BaseView(RefactorClient refactorClient, ModeClient modeClient) { this.refactorClient = refactorClient; this.modeClient = modeClient; - - setAlignItems(Alignment.CENTER); - setJustifyContentMode(JustifyContentMode.CENTER); - - add(getLogoImage()); setupUI(); } @@ -71,12 +64,4 @@ protected Set convertToSet(String commaSeparatedString) { .filter(s -> !s.isEmpty()) .collect(Collectors.toSet()); } - - private Image getLogoImage() { - StreamResource imageResource = new StreamResource("robert.png", - () -> getClass().getResourceAsStream("/static/robert.png")); - Image logo = new Image(imageResource, "Logo"); - logo.setWidth("240px"); - return logo; - } } \ No newline at end of file diff --git a/src/main/java/org/cftoolsuite/ui/view/ChatView.java b/src/main/java/org/cftoolsuite/ui/view/ChatView.java index 5ae4bd3..ccb56cd 100644 --- a/src/main/java/org/cftoolsuite/ui/view/ChatView.java +++ b/src/main/java/org/cftoolsuite/ui/view/ChatView.java @@ -1,135 +1,145 @@ package org.cftoolsuite.ui.view; -import org.cftoolsuite.client.ModeClient; -import org.cftoolsuite.client.RefactorClient; -import org.cftoolsuite.ui.MainLayout; -import org.cftoolsuite.ui.component.Markdown; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.http.ResponseEntity; - import com.vaadin.flow.component.Key; +import com.vaadin.flow.component.UI; import com.vaadin.flow.component.button.Button; -import com.vaadin.flow.component.html.Div; import com.vaadin.flow.component.html.H2; -import com.vaadin.flow.component.html.H4; -import com.vaadin.flow.component.notification.NotificationVariant; import com.vaadin.flow.component.orderedlayout.HorizontalLayout; import com.vaadin.flow.component.orderedlayout.VerticalLayout; -import com.vaadin.flow.component.progressbar.ProgressBar; import com.vaadin.flow.component.textfield.TextField; import com.vaadin.flow.router.PageTitle; import com.vaadin.flow.router.Route; +import org.apache.commons.collections4.CollectionUtils; +import org.cftoolsuite.client.ModeClient; +import org.cftoolsuite.client.RefactorStreamingClient; +import org.cftoolsuite.domain.chat.FilterMetadata; +import org.cftoolsuite.domain.chat.Inquiry; +import org.cftoolsuite.ui.MainLayout; +import org.cftoolsuite.ui.component.Markdown; +import org.cftoolsuite.ui.component.MetadataFilter; + +import java.util.List; @PageTitle("robert-ui ยป Chat") @Route(value = "chat", layout = MainLayout.class) -public class ChatView extends BaseView { - - private static final Logger log = LoggerFactory.getLogger(ChatView.class); +public class ChatView extends BaseStreamingView { - private Div messageList; - private TextField messageInput; - private Button sendButton; + private TextField question; + private MetadataFilter metadataFilter; + private Button submitButton; private Button clearButton; private HorizontalLayout buttons; - private ProgressBar typingIndicator; + private Markdown chatHistory; - public ChatView(RefactorClient refactorClient, ModeClient modeClient) { - super(refactorClient, modeClient); + public ChatView(RefactorStreamingClient refactorStreamingClient, ModeClient modeClient) { + super(refactorStreamingClient, modeClient); } @Override protected void setupUI() { + var ui = UI.getCurrent(); setSizeFull(); - setPadding(true); - setSpacing(true); - messageList = new Div(); - messageList.setClassName("message-list"); - messageList.getStyle().set("overflow-y", "auto"); - messageList.setHeight("95%"); + // Create chat history container + VerticalLayout chatHistoryContainer = new VerticalLayout(); + chatHistoryContainer.setSizeFull(); + chatHistoryContainer.setPadding(false); + chatHistoryContainer.setSpacing(false); - typingIndicator = new ProgressBar(); - typingIndicator.setIndeterminate(true); - typingIndicator.setVisible(false); + // Add Markdown with styling + chatHistory = new Markdown(); + chatHistory.getElement().getStyle() + .set("height", "100%") + .set("width", "100%") + .set("overflow", "auto"); - messageInput = new TextField(); - messageInput.setPlaceholder("Type a message..."); - messageInput.setWidth("100%"); + chatHistoryContainer.add(chatHistory); + chatHistoryContainer.setFlexGrow(1, chatHistory); - this.buttons = new HorizontalLayout(); - sendButton = new Button("Send"); - sendButton.addClickListener(e -> submitRequest()); - sendButton.addClickShortcut(Key.ENTER); + // Create input components + question = new TextField(); + question.setPlaceholder("Type a question..."); + question.setWidth("100%"); - this.clearButton = new Button("Clear"); - clearButton.addClickListener(e -> clearAllFields()); - buttons.add(sendButton, clearButton); + metadataFilter = new MetadataFilter(); + metadataFilter.setWidth("100%"); + metadataFilter.setLabel("Metadata Filters"); - messageInput.addValueChangeListener(event -> { - if (event.getValue().isEmpty()) { - hideThinkingIndicator(); - } + // Create buttons + this.buttons = new HorizontalLayout(); + submitButton = new Button("Submit"); + submitButton.addClickListener(e -> { + // Prepare the question and metadata message + String inquiry = formatQuestion( + question.getValue(), + metadataFilter.getValue() + ); + + // Clear previous content and append question message + chatHistory.clear(); + chatHistory.appendMarkdown(inquiry); + + // Stream the response + refactorStreamingClient + .streamResponseToQuestion(new Inquiry(question.getValue(), metadataFilter.getValue())) + .subscribe(ui.accessLater(response -> { + chatHistory.appendMarkdown("\n\n" + response); + }, null)); }); + submitButton.addClickShortcut(Key.ENTER); - add(new H2("Chat")); + this.clearButton = new Button("Clear"); + clearButton.addClickListener(e -> clearAllFields()); + buttons.add(submitButton, clearButton); - VerticalLayout inputLayout = new VerticalLayout(typingIndicator, messageInput, buttons); + // Create input layout with bottom-anchoring + VerticalLayout inputLayout = new VerticalLayout(question, metadataFilter, buttons); inputLayout.setSpacing(false); inputLayout.setPadding(false); + inputLayout.setWidth("100%"); - add(messageList, inputLayout); - } + // Create a wrapper layout to control height distribution + VerticalLayout mainLayout = new VerticalLayout(); + mainLayout.setSizeFull(); + mainLayout.setPadding(false); + mainLayout.setSpacing(false); - @Override - protected void submitRequest() { - String message = messageInput.getValue(); - if (!message.isEmpty()) { - addMessageToList("You:", message); - messageInput.clear(); - getAiBotResponse(message); - } - } + // Add header + H2 header = new H2("Chat"); + mainLayout.add(header); - @Override - protected void clearAllFields() { - messageInput.clear(); - messageList.removeAll(); - } + // Add chat history with expand ratio + mainLayout.add(chatHistoryContainer); + mainLayout.setFlexGrow(2, chatHistoryContainer); // This gives chatHistory 2/3 of the space - private void addMessageToList(String title, String message) { - H4 whom = new H4(title); - Markdown messageDiv = new Markdown(message); - messageList.add(whom, messageDiv); - } + // Add input layout at the bottom + mainLayout.add(inputLayout); + mainLayout.setFlexGrow(0, inputLayout); // This prevents input layout from expanding - private void showThinkingIndicator() { - typingIndicator.setVisible(true); + // Replace the direct add with adding the main layout + add(mainLayout); } - private void hideThinkingIndicator() { - typingIndicator.setVisible(false); + private String formatQuestion(String question, List metadata) { + StringBuilder messageBuilder = new StringBuilder(); + messageBuilder.append("**").append(question).append("**").append("\n\n"); + if (CollectionUtils.isNotEmpty(metadata)) { + messageBuilder.append("Filtered by: { "); + metadata.forEach(fm -> + messageBuilder.append(fm.key()) + .append(": ").append(fm.value()).append(" | ") + ); + messageBuilder.append(" }"); + } + String formattedQuestion = messageBuilder.toString(); + if (formattedQuestion.endsWith("| }")) formattedQuestion = formattedQuestion.replaceAll("\\|\\s*}", "}"); + return formattedQuestion; } - private void getAiBotResponse(String message) { - showThinkingIndicator(); - try { - ResponseEntity response = refactorClient.chat(message); - if (response.getStatusCode().is2xxSuccessful()) { - addMessageToList("AI ChatBot:", response.getBody()); - } else { - String errorMessage = "Error submitting chat request. Status code: " + response.getStatusCode(); - if (response.getBody() != null) { - errorMessage += ". Message: " + response.getBody().toString(); - } - showNotification(errorMessage, NotificationVariant.LUMO_ERROR); - } - hideThinkingIndicator(); - } catch (Exception e) { - String errorMessage = "An unexpected error occurred: " + e.getMessage(); - hideThinkingIndicator(); - showNotification(errorMessage, NotificationVariant.LUMO_ERROR); - log.error("An unexpected error occurred", e); - } + @Override + protected void clearAllFields() { + question.clear(); + chatHistory.clear(); + metadataFilter.clear(); } } diff --git a/src/main/java/org/cftoolsuite/ui/view/IngestView.java b/src/main/java/org/cftoolsuite/ui/view/IngestView.java index 3e2bfcc..a616639 100644 --- a/src/main/java/org/cftoolsuite/ui/view/IngestView.java +++ b/src/main/java/org/cftoolsuite/ui/view/IngestView.java @@ -68,8 +68,6 @@ protected void setupUI() { initializeAllowedExtensionsComboBox(); - buttons.setAlignItems(Alignment.CENTER); - buttons.setJustifyContentMode(JustifyContentMode.CENTER); submitButton.addClickListener(event -> submitRequest()); clearButton.addClickListener(event -> clearAllFields()); diff --git a/src/main/java/org/cftoolsuite/ui/view/RefactorView.java b/src/main/java/org/cftoolsuite/ui/view/RefactorView.java index c939b28..20a4a4c 100644 --- a/src/main/java/org/cftoolsuite/ui/view/RefactorView.java +++ b/src/main/java/org/cftoolsuite/ui/view/RefactorView.java @@ -104,8 +104,6 @@ protected void setupUI() { initializeAllowedExtensionsComboBox(); - buttons.setAlignItems(Alignment.CENTER); - buttons.setJustifyContentMode(JustifyContentMode.CENTER); submitButton.addClickListener(e -> submitRequest()); clearButton.addClickListener(e -> clearAllFields()); diff --git a/src/main/java/org/cftoolsuite/ui/view/SearchView.java b/src/main/java/org/cftoolsuite/ui/view/SearchView.java index 4b8c9da..4b85e0b 100644 --- a/src/main/java/org/cftoolsuite/ui/view/SearchView.java +++ b/src/main/java/org/cftoolsuite/ui/view/SearchView.java @@ -83,8 +83,6 @@ protected void setupUI() { this.allowedExtensions = refactorClient.languageExtensions().getBody(); initializeAllowedExtensionsComboBox(); - buttons.setAlignItems(Alignment.CENTER); - buttons.setJustifyContentMode(JustifyContentMode.CENTER); submitButton.addClickListener(event -> submitRequest()); clearButton.addClickListener(event -> clearAllFields()); diff --git a/src/test/java/org/cftoolsuite/RobertUiApplicationTests.java b/src/test/java/org/cftoolsuite/RobertUiApplicationTests.java deleted file mode 100644 index ec72cc8..0000000 --- a/src/test/java/org/cftoolsuite/RobertUiApplicationTests.java +++ /dev/null @@ -1,13 +0,0 @@ -package org.cftoolsuite; - -import org.junit.jupiter.api.Test; -import org.springframework.boot.test.context.SpringBootTest; - -@SpringBootTest -class RobertUiApplicationTests { - - @Test - void contextLoads() { - } - -}