From 0fce4a99fc4452e621e093066fbdd21a3e3124c4 Mon Sep 17 00:00:00 2001 From: Carl-Robert Linnupuu Date: Wed, 12 Apr 2023 20:31:40 +0100 Subject: [PATCH] Ability to delete previous messages --- .../carlrobert/codegpt/action/BaseAction.java | 3 +- .../state/conversations/Conversation.java | 8 +++ .../state/conversations/message/Message.java | 8 ++- .../chat/BrowserContentManager.java | 14 +++-- .../chat/ChatToolWindowTabHtmlPanel.java | 29 +++++----- .../chat/ChatToolWindowTabPanel.java | 25 ++++----- .../chat/MarkdownJCEFHtmlPanel.java | 55 ++++++++++++++----- .../toolwindow/chat/ToolWindowTabPanel.java | 5 +- src/main/resources/html/index.html | 12 ++-- src/main/resources/html/js/main.js | 29 +++++++--- 10 files changed, 122 insertions(+), 66 deletions(-) diff --git a/src/main/java/ee/carlrobert/codegpt/action/BaseAction.java b/src/main/java/ee/carlrobert/codegpt/action/BaseAction.java index 729d3721..f27eaad5 100644 --- a/src/main/java/ee/carlrobert/codegpt/action/BaseAction.java +++ b/src/main/java/ee/carlrobert/codegpt/action/BaseAction.java @@ -6,6 +6,7 @@ import com.intellij.openapi.actionSystem.PlatformDataKeys; import com.intellij.openapi.editor.Editor; import com.intellij.openapi.project.Project; import com.intellij.openapi.util.NlsActions; +import ee.carlrobert.codegpt.state.conversations.message.Message; import ee.carlrobert.codegpt.toolwindow.chat.ChatContentManagerService; import javax.swing.Icon; import org.jetbrains.annotations.NotNull; @@ -47,7 +48,7 @@ public abstract class BaseAction extends AnAction { protected void sendMessage(@NotNull Project project, String prompt) { var newTabPanel = project.getService(ChatContentManagerService.class).createNewTabPanel(); if (newTabPanel != null) { - newTabPanel.startNewConversation(prompt); + newTabPanel.startNewConversation(new Message(prompt)); } } } diff --git a/src/main/java/ee/carlrobert/codegpt/state/conversations/Conversation.java b/src/main/java/ee/carlrobert/codegpt/state/conversations/Conversation.java index 06003860..f9f61d1e 100644 --- a/src/main/java/ee/carlrobert/codegpt/state/conversations/Conversation.java +++ b/src/main/java/ee/carlrobert/codegpt/state/conversations/Conversation.java @@ -1,5 +1,7 @@ package ee.carlrobert.codegpt.state.conversations; +import static java.util.stream.Collectors.toList; + import ee.carlrobert.codegpt.state.conversations.message.Message; import ee.carlrobert.openai.client.ClientCode; import java.time.LocalDateTime; @@ -67,4 +69,10 @@ public class Conversation { public void setUpdatedOn(LocalDateTime updatedOn) { this.updatedOn = updatedOn; } + + public void removeMessage(UUID messageId) { + setMessages(messages.stream() + .filter(message -> !message.getId().equals(messageId)) + .collect(toList())); + } } diff --git a/src/main/java/ee/carlrobert/codegpt/state/conversations/message/Message.java b/src/main/java/ee/carlrobert/codegpt/state/conversations/message/Message.java index 7be265c8..f087c129 100644 --- a/src/main/java/ee/carlrobert/codegpt/state/conversations/message/Message.java +++ b/src/main/java/ee/carlrobert/codegpt/state/conversations/message/Message.java @@ -2,20 +2,22 @@ package ee.carlrobert.codegpt.state.conversations.message; import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.UUID; public class Message { + private final UUID id; private final String prompt; private String response; @JsonCreator(mode = JsonCreator.Mode.PROPERTIES) public Message(@JsonProperty("prompt") String prompt) { + this.id = UUID.randomUUID(); this.prompt = prompt; } - public Message(String prompt, String response) { - this.prompt = prompt; - this.response = response; + public UUID getId() { + return id; } public String getPrompt() { diff --git a/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/BrowserContentManager.java b/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/BrowserContentManager.java index 9d9e9487..15daf72c 100644 --- a/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/BrowserContentManager.java +++ b/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/BrowserContentManager.java @@ -6,6 +6,7 @@ import static org.apache.commons.text.StringEscapeUtils.escapeEcmaScript; import com.vladsch.flexmark.html.HtmlRenderer; import com.vladsch.flexmark.parser.Parser; import com.vladsch.flexmark.util.data.MutableDataSet; +import ee.carlrobert.codegpt.state.conversations.message.Message; import java.util.UUID; import org.cef.browser.CefBrowser; @@ -17,12 +18,13 @@ public class BrowserContentManager { this.browser = browser; } - public void displayUserMessage(String accountName, String prompt) { - executeJS("window.CodeGPTBridge.displayUserMessage(?, ?, ?);", accountName, convertToHtml(prompt), getHtmlSvgIcon("delete-icon")); + public void displayUserMessage(String accountName, Message message) { + executeJS("window.CodeGPTBridge.prepareMessage(?);", message.getId()); + executeJS("window.CodeGPTBridge.displayUserMessage(?, ?, ?, ?);", message.getId(), accountName, convertToHtml(message.getPrompt()), getHtmlSvgIcon("delete-icon")); } - public void displayResponse(UUID responseId, boolean animate) { - executeJS("window.CodeGPTBridge.displayResponse(?, ?, ?);", responseId, animate, getHtmlSvgIcon("codegpt-icon")); + public void displayResponse(UUID messageId, UUID responseId, boolean animate) { + executeJS("window.CodeGPTBridge.displayResponse(?, ?, ?, ?);", messageId, responseId, animate, getHtmlSvgIcon("codegpt-icon")); } public void replaceResponseContent(UUID responseId, String message) { @@ -57,6 +59,10 @@ public class BrowserContentManager { executeJS("window.CodeGPTBridge.updateRegenerateButton(?, ?);", title, isDisabled); } + public void deleteMessage(UUID messageId) { + executeJS("window.CodeGPTBridge.deleteMessage(?);", messageId); + } + private void executeJS(String query, Object... params) { browser.executeJavaScript(formatQuery(query, params), null, 0); } diff --git a/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ChatToolWindowTabHtmlPanel.java b/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ChatToolWindowTabHtmlPanel.java index bb9a72e0..a05485d4 100644 --- a/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ChatToolWindowTabHtmlPanel.java +++ b/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ChatToolWindowTabHtmlPanel.java @@ -62,22 +62,22 @@ public class ChatToolWindowTabHtmlPanel implements ToolWindowTabPanel { } @Override - public void startNewConversation(String prompt) { + public void startNewConversation(Message message) { markdownHtmlPanel.runWhenLoaded(() -> { conversation = ConversationsState.getInstance().startConversation(); - startConversation(prompt, false); + startConversation(message, false); }); } @Override - public void startConversation(String prompt, boolean isRetry) { + public void startConversation(Message message, boolean isRetry) { markdownHtmlPanel.runWhenLoaded(() -> { - markdownHtmlPanel.displayUserMessage(prompt); + markdownHtmlPanel.displayUserMessage(message); var settings = SettingsState.getInstance(); if (settings.apiKey.isEmpty()) { - markdownHtmlPanel.displayMissingCredential(); + markdownHtmlPanel.displayMissingCredential(message.getId()); } else { - SwingUtilities.invokeLater(() -> call(prompt, isRetry)); + SwingUtilities.invokeLater(() -> call(message, isRetry)); } }); } @@ -95,15 +95,14 @@ public class ChatToolWindowTabHtmlPanel implements ToolWindowTabPanel { markdownHtmlPanel.dispose(); } - private void call(String prompt, boolean isRetry) { + private void call(Message message, boolean isRetry) { if (conversation == null) { conversation = ConversationsState.getInstance().startConversation(); } - var responseId = markdownHtmlPanel.prepareResponse(true, isRetry); - var conversationMessage = new Message(prompt); + var responseId = markdownHtmlPanel.prepareResponse(message.getId(), true, isRetry); var requestService = new RequestHandler(conversation) { - public void handleMessage(String message, String fullMessage) { + public void handleMessage(String response, String fullMessage) { try { markdownHtmlPanel.replaceHtml(responseId, fullMessage); } catch (Exception e) { @@ -114,7 +113,7 @@ public class ChatToolWindowTabHtmlPanel implements ToolWindowTabPanel { } public void handleComplete() { - stopGenerating(prompt); + stopGenerating(message); markdownHtmlPanel.stopTyping(); markdownHtmlPanel.updateReplaceButton(); } @@ -123,12 +122,12 @@ public class ChatToolWindowTabHtmlPanel implements ToolWindowTabPanel { markdownHtmlPanel.displayErrorMessage(errorMessage); } }; - requestService.call(conversationMessage, isRetry); + requestService.call(message, isRetry); displayReloadResponseButton(requestService::cancel); } private void handleSubmit() { - startConversation(textArea.getText(), false); + startConversation(new Message(textArea.getText()), false); textArea.setText(""); } @@ -138,8 +137,8 @@ public class ChatToolWindowTabHtmlPanel implements ToolWindowTabPanel { button.setMode(GenerateButton.Mode.STOP, onClick); } - private void stopGenerating(String prompt) { - getReloadResponseButton().setMode(GenerateButton.Mode.REFRESH, () -> call(prompt, true)); + private void stopGenerating(Message message) { + getReloadResponseButton().setMode(GenerateButton.Mode.REFRESH, () -> call(message, true)); } private GenerateButton getReloadResponseButton() { diff --git a/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ChatToolWindowTabPanel.java b/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ChatToolWindowTabPanel.java index 319093a4..bdf3265c 100644 --- a/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ChatToolWindowTabPanel.java +++ b/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ChatToolWindowTabPanel.java @@ -114,13 +114,13 @@ public class ChatToolWindowTabPanel implements ToolWindowTabPanel { } @Override - public void startNewConversation(String prompt) { + public void startNewConversation(Message message) { conversation = ConversationsState.getInstance().startConversation(); - startConversation(prompt, false); + startConversation(message, false); } @Override - public void startConversation(String prompt, boolean isRetry) { + public void startConversation(Message message, boolean isRetry) { if (!isRetry) { addIconLabel(Icons.DefaultImageIcon, "ChatGPT"); } @@ -138,7 +138,7 @@ public class ChatToolWindowTabPanel implements ToolWindowTabPanel { scrollablePanel.add(textArea); textAreas.add(textArea); } - call(textArea, prompt, isRetry); + call(textArea, message, isRetry); } } @@ -153,12 +153,11 @@ public class ChatToolWindowTabPanel implements ToolWindowTabPanel { scrollablePanel.repaint(); } - public void call(SyntaxTextArea textArea, String prompt, boolean isRetry) { + public void call(SyntaxTextArea textArea, Message message, boolean isRetry) { if (conversation == null) { conversation = ConversationsState.getInstance().startConversation(); } - var conversationMessage = new Message(prompt); var requestService = new RequestHandler(conversation) { public void handleMessage(String message, String fullMessage) { try { @@ -172,14 +171,14 @@ public class ChatToolWindowTabPanel implements ToolWindowTabPanel { } public void handleComplete() { - stopGenerating(prompt, textArea); + stopGenerating(message, textArea); } public void handleError(String errorMessage) { textArea.append(errorMessage); } }; - requestService.call(conversationMessage, isRetry); + requestService.call(message, isRetry); displayGenerateButton(requestService::cancel); } @@ -216,9 +215,9 @@ public class ChatToolWindowTabPanel implements ToolWindowTabPanel { generateButton.setMode(GenerateButton.Mode.STOP, onClick); } - public void stopGenerating(String prompt, SyntaxTextArea textArea) { + public void stopGenerating(Message message, SyntaxTextArea textArea) { generateButton.setMode(GenerateButton.Mode.REFRESH, () -> { - startConversation(prompt, true); + startConversation(message, true); scrollToBottom(); }); textArea.displayCopyButton(); @@ -231,15 +230,15 @@ public class ChatToolWindowTabPanel implements ToolWindowTabPanel { } private void handleSubmit() { - var searchText = textArea.getText(); + var message = new Message(textArea.getText()); if (isLandingViewVisible) { clearWindow(); } if (ConversationsState.getCurrentConversation() == null) { setConversation(ConversationsState.getInstance().startConversation()); } - displayUserMessage(searchText); - startNewConversation(searchText); + displayUserMessage(message.getPrompt()); + startNewConversation(message); textArea.setText(""); scrollToBottom(); scrollablePanel.revalidate(); diff --git a/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/MarkdownJCEFHtmlPanel.java b/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/MarkdownJCEFHtmlPanel.java index f7b63840..56fb7604 100644 --- a/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/MarkdownJCEFHtmlPanel.java +++ b/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/MarkdownJCEFHtmlPanel.java @@ -8,17 +8,21 @@ import static ee.carlrobert.codegpt.util.ThemeUtils.getFontColorRGB; import static ee.carlrobert.codegpt.util.ThemeUtils.getFontSize; import static ee.carlrobert.codegpt.util.ThemeUtils.getPanelBackgroundColorRGB; import static ee.carlrobert.codegpt.util.ThemeUtils.getSeparatorColorRGB; +import static icons.Icons.DefaultImageIcon; import static java.lang.String.format; import com.intellij.openapi.application.ApplicationManager; import com.intellij.openapi.fileEditor.FileEditorManager; import com.intellij.openapi.project.Project; +import com.intellij.openapi.ui.Messages; import com.intellij.ui.jcef.JBCefBrowserBase; import com.intellij.ui.jcef.JBCefJSQuery; import com.intellij.ui.jcef.JCEFHtmlPanel; import com.intellij.util.ui.UIUtil; import ee.carlrobert.codegpt.state.AccountDetailsState; import ee.carlrobert.codegpt.state.conversations.Conversation; +import ee.carlrobert.codegpt.state.conversations.ConversationsState; +import ee.carlrobert.codegpt.state.conversations.message.Message; import ee.carlrobert.codegpt.util.FileUtils; import java.awt.Toolkit; import java.awt.datatransfer.Clipboard; @@ -29,6 +33,7 @@ import java.util.Map; import java.util.UUID; import java.util.concurrent.CompletableFuture; import java.util.stream.Collectors; +import javax.swing.SwingUtilities; import javax.swing.UIManager; import org.cef.browser.CefBrowser; import org.cef.browser.CefFrame; @@ -39,9 +44,10 @@ public class MarkdownJCEFHtmlPanel extends JCEFHtmlPanel { private final CompletableFuture isLoaded = new CompletableFuture<>(); private final JBCefJSQuery copyCodeQuery = JBCefJSQuery.create((JBCefBrowserBase) this); + private final JBCefJSQuery deleteMessageQuery = JBCefJSQuery.create((JBCefBrowserBase) this); + private final JBCefJSQuery replaceInEditorQuery; private final BrowserContentManager browserContentManager; private final Project project; - private final JBCefJSQuery replaceInEditorQuery; private UUID previousResponseId; public MarkdownJCEFHtmlPanel(@NotNull Project project) { @@ -55,24 +61,42 @@ public class MarkdownJCEFHtmlPanel extends JCEFHtmlPanel { clipboard.setContents(stringSelection, null); return null; }); + this.deleteMessageQuery.addHandler((messageId) -> { + SwingUtilities.invokeLater(() -> { + int answer = Messages.showYesNoDialog("Are you sure you want to delete this message?", "Delete Message", DefaultImageIcon); + if (answer == Messages.YES) { + browserContentManager.deleteMessage(UUID.fromString(messageId)); + var conversation = ConversationsState.getCurrentConversation(); + if (conversation != null) { + conversation.removeMessage(UUID.fromString(messageId)); + ConversationsState.getInstance().saveConversation(conversation); + } + } + }); + return null; + }); setHtml(getIndexContent()); addBrowserLoadHandler(); addLookAndFeelChangeListener(); } - public void displayUserMessage(String prompt) { + public void displayUserMessage(Message message) { var name = AccountDetailsState.getInstance().accountName; - browserContentManager.displayUserMessage(name == null || name.isEmpty() ? "User" : name, prompt); + browserContentManager.displayUserMessage(name == null || name.isEmpty() ? "User" : name, message); } - public UUID prepareResponse(boolean animate, boolean isRetry) { + public UUID prepareResponse(UUID messageId) { + return prepareResponse(messageId, false, false); + } + + public UUID prepareResponse(UUID messageId, boolean animate, boolean isRetry) { if (isRetry) { browserContentManager.clearResponse(previousResponseId); browserContentManager.animateSvg(previousResponseId); } else { previousResponseId = UUID.randomUUID(); - browserContentManager.displayResponse(previousResponseId, animate); + browserContentManager.displayResponse(messageId, previousResponseId, animate); } return previousResponseId; } @@ -80,8 +104,8 @@ public class MarkdownJCEFHtmlPanel extends JCEFHtmlPanel { public void displayConversation(Conversation conversation) { runWhenLoaded(() -> { conversation.getMessages().forEach(message -> { - displayUserMessage(message.getPrompt()); - replaceHtml(prepareResponse(false, false), message.getResponse()); + displayUserMessage(message); + replaceHtml(prepareResponse(message.getId()), message.getResponse()); }); updateReplaceButton(); }); @@ -95,8 +119,8 @@ public class MarkdownJCEFHtmlPanel extends JCEFHtmlPanel { browserContentManager.displayErrorMessage(previousResponseId, errorMessage); } - public void displayMissingCredential() { - browserContentManager.displayMissingCredential(prepareResponse(false, false)); + public void displayMissingCredential(UUID messageId) { + browserContentManager.displayMissingCredential(prepareResponse(messageId)); } public void displayLandingView() { @@ -134,13 +158,16 @@ public class MarkdownJCEFHtmlPanel extends JCEFHtmlPanel { if (httpStatusCode == 200) { myCefBrowser.executeJavaScript( "window.JavaPanelBridge = {" + - "copyCode : function(responseId) {" + - copyCodeQuery.inject("responseId") + + "copyCode : function(code) {" + + copyCodeQuery.inject("code") + "}," + - "replaceCode : function(responseId) {" + - replaceInEditorQuery.inject("responseId") + + "replaceCode : function(code) {" + + replaceInEditorQuery.inject("code") + + "}," + + "deleteMessage : function(messageId) {" + + deleteMessageQuery.inject("messageId") + "}" + - "};", + "};", myCefBrowser.getURL(), 0); myCefBrowser.executeJavaScript(FileUtils.getResource("/html/js/main.js"), myCefBrowser.getURL(), 0); isLoaded.complete(null); diff --git a/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ToolWindowTabPanel.java b/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ToolWindowTabPanel.java index 41317da9..e700e2c8 100644 --- a/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ToolWindowTabPanel.java +++ b/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ToolWindowTabPanel.java @@ -1,6 +1,7 @@ package ee.carlrobert.codegpt.toolwindow.chat; import ee.carlrobert.codegpt.state.conversations.Conversation; +import ee.carlrobert.codegpt.state.conversations.message.Message; import javax.swing.JPanel; public interface ToolWindowTabPanel { @@ -13,7 +14,7 @@ public interface ToolWindowTabPanel { void displayConversation(Conversation conversation); - void startNewConversation(String prompt); + void startNewConversation(Message message); - void startConversation(String prompt, boolean isRetry); + void startConversation(Message message, boolean isRetry); } diff --git a/src/main/resources/html/index.html b/src/main/resources/html/index.html index 6e7ff12a..978f442f 100644 --- a/src/main/resources/html/index.html +++ b/src/main/resources/html/index.html @@ -147,14 +147,14 @@ display: flex; justify-content: space-between; align-items: center; + margin-block-end: 0.5em; } - .user-message-actions { - display: flex; - gap: 8px; + .user-prompt > p:first-child { + margin-block-start: 0.5em; } - .user-message-actions > button { + .delete-button { background-color: var(--bg); display: inline-flex; border: 1px solid var(--separator-color); @@ -164,11 +164,11 @@ transition: background-color 150ms ease-in; } - .user-message-actions > button:hover { + .delete-button:hover { background-color: var(--panel-background-color); } - .user-message-actions > button path, polygon { + .delete-button path, polygon { color: var(--font-color); } diff --git a/src/main/resources/html/js/main.js b/src/main/resources/html/js/main.js index 56bc43cc..57120131 100644 --- a/src/main/resources/html/js/main.js +++ b/src/main/resources/html/js/main.js @@ -18,21 +18,28 @@ window.CodeGPTBridge = { })); document.body.appendChild(wrapper); }, - displayUserMessage: function (accountName, htmlMessage, deleteSvgIcon) { + prepareMessage: function (messageId) { + const wrapper = createElement({tagName: 'div', className: 'message'}); + wrapper.setAttribute("id", messageId); + document.body.appendChild(wrapper); + }, + displayUserMessage: function (messageId, accountName, htmlMessage, deleteSvgIcon) { document.getElementById("landing-view")?.remove(); - const actions = createElement({tagName: 'span', className: 'user-message-actions'}); - actions.appendChild(createElement({tagName: 'button', className: 'delete-icon-wrapper', innerHTML: deleteSvgIcon})); + const deleteButton = createElement({tagName: 'button', className: 'delete-button', innerHTML: deleteSvgIcon}); + deleteButton.addEventListener("click", function () { + window.JavaPanelBridge.deleteMessage(messageId) + }); const nameWrapper = createElement({tagName: 'p', className: 'user-message-name-wrapper'}); nameWrapper.appendChild(createElement({tagName: 'span', textContent: accountName})); - nameWrapper.appendChild(actions); + nameWrapper.appendChild(deleteButton); const wrapper = createElement({tagName: 'div', className: 'user-message'}); wrapper.appendChild(nameWrapper); - wrapper.appendChild(createElement({tagName: 'div', innerHTML: htmlMessage})); - document.body.appendChild(wrapper); + wrapper.appendChild(createElement({tagName: 'div', className: 'user-prompt', innerHTML: htmlMessage})); + document.getElementById(messageId)?.appendChild(wrapper); scrollToBottom(); Prism.highlightAll(); }, @@ -44,7 +51,7 @@ window.CodeGPTBridge = { // TODO: Add anchor link for opening the settings panel responseWrapper.innerHTML = "

API key not provided.

"; }, - displayResponse: function (responseId, animate, svgIcon) { + displayResponse: function (messageId, responseId, animate, svgIcon) { const wrapper = createElement({tagName: 'div', className: 'response'}) const iconLabelContainer = document.createElement('p'); @@ -62,7 +69,7 @@ window.CodeGPTBridge = { const responseWrapper = createElement({tagName: 'div', innerHTML: "

"}) responseWrapper.setAttribute('id', responseId); wrapper.appendChild(responseWrapper); - document.body.appendChild(wrapper); + document.getElementById(messageId)?.appendChild(wrapper); scrollToBottom(); }, clearResponse: function (responseId) { @@ -92,6 +99,12 @@ window.CodeGPTBridge = { for (const icon of icons) { icon.style = null; } + }, + deleteMessage: function (messageId) { + document.getElementById(messageId)?.remove(); + if (document.getElementsByClassName('message').length === 0) { + this.displayLandingView(); + } } }