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 extends Component> 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() {
- }
-
-}