From 0fd61a7135a703085bd317e77b705227ed55b49d Mon Sep 17 00:00:00 2001 From: Carl-Robert Linnupuu Date: Sat, 1 Apr 2023 15:02:00 +0100 Subject: [PATCH] Logic for parsing markup and rendering the content in CefBrowser --- .../codegpt/action/ActionsUtil.java | 6 +- .../carlrobert/codegpt/action/BaseAction.java | 5 +- .../codegpt/action/CustomPromptAction.java | 15 +- .../codegpt/action/CustomPromptDialog.java | 5 +- .../codegpt/client/ActionListener.java | 2 +- .../client/CompletionRequestProvider.java | 4 +- .../codegpt/client/EventListener.java | 1 - .../codegpt/client/RequestHandler.java | 23 ++- .../state/settings/SettingsConfigurable.java | 4 +- .../chat/BrowserContentManager.java | 189 ++++++++++++++++++ .../chat/ChatContentManagerService.java | 6 +- .../toolwindow/chat/ChatTabbedPane.java | 9 +- .../toolwindow/chat/ChatToolWindowPanel.java | 10 +- .../chat/ChatToolWindowTabHtmlPanel.form | 45 +++++ .../chat/ChatToolWindowTabHtmlPanel.java | 151 ++++++++++++++ .../chat/ChatToolWindowTabPanel.java | 46 +++-- .../toolwindow/chat/HtmlPanelPopupMenu.java | 51 +++++ .../chat/MarkdownJCEFHtmlPanel.java | 187 +++++++++++++++++ .../toolwindow/chat/ToolWindowTabPanel.java | 20 ++ .../chat/ToolWindowTabPanelFactory.java | 15 ++ .../toolwindow/components/ScrollPane.java | 3 +- .../toolwindow/components/TextArea.java | 25 +-- .../ConversationsToolWindow.java | 4 +- .../ee/carlrobert/codegpt/util/FileUtils.java | 17 ++ 24 files changed, 752 insertions(+), 91 deletions(-) create mode 100644 src/main/java/ee/carlrobert/codegpt/toolwindow/chat/BrowserContentManager.java create mode 100644 src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ChatToolWindowTabHtmlPanel.form create mode 100644 src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ChatToolWindowTabHtmlPanel.java create mode 100644 src/main/java/ee/carlrobert/codegpt/toolwindow/chat/HtmlPanelPopupMenu.java create mode 100644 src/main/java/ee/carlrobert/codegpt/toolwindow/chat/MarkdownJCEFHtmlPanel.java create mode 100644 src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ToolWindowTabPanel.java create mode 100644 src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ToolWindowTabPanelFactory.java create mode 100644 src/main/java/ee/carlrobert/codegpt/util/FileUtils.java diff --git a/src/main/java/ee/carlrobert/codegpt/action/ActionsUtil.java b/src/main/java/ee/carlrobert/codegpt/action/ActionsUtil.java index b8033b9e..b13a48f5 100644 --- a/src/main/java/ee/carlrobert/codegpt/action/ActionsUtil.java +++ b/src/main/java/ee/carlrobert/codegpt/action/ActionsUtil.java @@ -7,7 +7,9 @@ import com.intellij.openapi.actionSystem.ActionManager; import com.intellij.openapi.actionSystem.AnAction; import com.intellij.openapi.actionSystem.DefaultActionGroup; import com.intellij.openapi.editor.Editor; +import com.intellij.openapi.editor.impl.EditorImpl; import com.intellij.openapi.project.Project; +import ee.carlrobert.codegpt.util.FileUtils; import java.util.LinkedHashMap; import java.util.Map; @@ -43,7 +45,9 @@ public class ActionsUtil { tableData.forEach((action, prompt) -> group.add(new BaseAction(action) { @Override protected void actionPerformed(Project project, Editor editor, String selectedText) { - sendMessage(project, prompt.replace("{{selectedCode}}", format("\n\n%s", selectedText))); + var fileExtension = FileUtils.getFileExtension(((EditorImpl) editor).getVirtualFile().getName()); + // TODO: Requires more sophisticated language parsing(can't always rely on the file extension) + sendMessage(project, prompt.replace("{{selectedCode}}", format("\n```%s\n%s\n```", fileExtension, selectedText))); } })); } diff --git a/src/main/java/ee/carlrobert/codegpt/action/BaseAction.java b/src/main/java/ee/carlrobert/codegpt/action/BaseAction.java index 0bc4cf74..729d3721 100644 --- a/src/main/java/ee/carlrobert/codegpt/action/BaseAction.java +++ b/src/main/java/ee/carlrobert/codegpt/action/BaseAction.java @@ -6,7 +6,6 @@ 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.ConversationsState; import ee.carlrobert.codegpt.toolwindow.chat.ChatContentManagerService; import javax.swing.Icon; import org.jetbrains.annotations.NotNull; @@ -48,9 +47,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.setConversation(ConversationsState.getInstance().startConversation()); - newTabPanel.displayUserMessage(prompt); - newTabPanel.sendMessage(prompt, project); + newTabPanel.startNewConversation(prompt); } } } diff --git a/src/main/java/ee/carlrobert/codegpt/action/CustomPromptAction.java b/src/main/java/ee/carlrobert/codegpt/action/CustomPromptAction.java index dad3bd20..ab24bb1b 100644 --- a/src/main/java/ee/carlrobert/codegpt/action/CustomPromptAction.java +++ b/src/main/java/ee/carlrobert/codegpt/action/CustomPromptAction.java @@ -4,8 +4,7 @@ import com.intellij.icons.AllIcons; import com.intellij.openapi.editor.Editor; import com.intellij.openapi.editor.impl.EditorImpl; import com.intellij.openapi.project.Project; -import java.util.regex.Matcher; -import java.util.regex.Pattern; +import ee.carlrobert.codegpt.util.FileUtils; import javax.swing.SwingUtilities; public class CustomPromptAction extends BaseAction { @@ -18,7 +17,7 @@ public class CustomPromptAction extends BaseAction { protected void actionPerformed(Project project, Editor editor, String selectedText) { if (selectedText != null && !selectedText.isEmpty()) { - var fileExtension = getFileExtension(((EditorImpl) editor).getVirtualFile().getName()); + var fileExtension = FileUtils.getFileExtension(((EditorImpl) editor).getVirtualFile().getName()); var dialog = new CustomPromptDialog(selectedText, fileExtension, previousUserPrompt); if (dialog.showAndGet()) { previousUserPrompt = dialog.getUserPrompt(); @@ -26,14 +25,4 @@ public class CustomPromptAction extends BaseAction { } } } - - private String getFileExtension(String filename) { - Pattern pattern = Pattern.compile("[^.]+$"); - Matcher matcher = pattern.matcher(filename); - - if (matcher.find()) { - return matcher.group(); - } - return null; - } } diff --git a/src/main/java/ee/carlrobert/codegpt/action/CustomPromptDialog.java b/src/main/java/ee/carlrobert/codegpt/action/CustomPromptDialog.java index cfda24e7..aa6f4d5c 100644 --- a/src/main/java/ee/carlrobert/codegpt/action/CustomPromptDialog.java +++ b/src/main/java/ee/carlrobert/codegpt/action/CustomPromptDialog.java @@ -1,6 +1,7 @@ package ee.carlrobert.codegpt.action; import static ee.carlrobert.codegpt.util.SwingUtils.addShiftEnterInputMap; +import static java.lang.String.format; import com.intellij.openapi.ui.DialogWrapper; import com.intellij.ui.components.JBScrollPane; @@ -28,7 +29,7 @@ public class CustomPromptDialog extends DialogWrapper { this.userPromptTextArea = new JTextArea(previousUserPrompt); this.userPromptTextArea.setCaretPosition(previousUserPrompt.length()); setTitle("Custom Prompt"); - setSize(460, 320); + setSize(600, 400); init(); } @@ -62,7 +63,7 @@ public class CustomPromptDialog extends DialogWrapper { } public String getFullPrompt() { - return userPromptTextArea.getText() + "\n\n" + syntaxTextArea.getText(); + return userPromptTextArea.getText() + format("\n```%s\n%s\n```", fileExtension, syntaxTextArea.getText()); } public String getUserPrompt() { diff --git a/src/main/java/ee/carlrobert/codegpt/client/ActionListener.java b/src/main/java/ee/carlrobert/codegpt/client/ActionListener.java index 5eeed6ca..d13ec9e6 100644 --- a/src/main/java/ee/carlrobert/codegpt/client/ActionListener.java +++ b/src/main/java/ee/carlrobert/codegpt/client/ActionListener.java @@ -7,6 +7,6 @@ interface ActionListener { default void handleError(String errorMessage) { } - default void handleMessage(String message) { + default void handleMessage(String message, String fullMessage) { } } diff --git a/src/main/java/ee/carlrobert/codegpt/client/CompletionRequestProvider.java b/src/main/java/ee/carlrobert/codegpt/client/CompletionRequestProvider.java index 238d84ee..83083829 100644 --- a/src/main/java/ee/carlrobert/codegpt/client/CompletionRequestProvider.java +++ b/src/main/java/ee/carlrobert/codegpt/client/CompletionRequestProvider.java @@ -35,7 +35,9 @@ class CompletionRequestProvider { var messages = new ArrayList(); messages.add(new ChatCompletionMessage( "system", - "You are ChatGPT, a large language model trained by OpenAI. Answer as concisely as possible.")); + "You are ChatGPT, a large language model trained by OpenAI. " + + "Answer as concisely as possible. " + + "Include code language in markdown snippets whenever possible.")); conversation.getMessages().forEach(message -> { messages.add(new ChatCompletionMessage("user", message.getPrompt())); messages.add(new ChatCompletionMessage("assistant", message.getResponse())); diff --git a/src/main/java/ee/carlrobert/codegpt/client/EventListener.java b/src/main/java/ee/carlrobert/codegpt/client/EventListener.java index 26afb4b8..c9dadde9 100644 --- a/src/main/java/ee/carlrobert/codegpt/client/EventListener.java +++ b/src/main/java/ee/carlrobert/codegpt/client/EventListener.java @@ -49,7 +49,6 @@ class EventListener implements CompletionEventListener { } message.setResponse(response); - conversation.addMessage(message); conversationsState.saveConversation(conversation); } } diff --git a/src/main/java/ee/carlrobert/codegpt/client/RequestHandler.java b/src/main/java/ee/carlrobert/codegpt/client/RequestHandler.java index ba823c6f..69d5be55 100644 --- a/src/main/java/ee/carlrobert/codegpt/client/RequestHandler.java +++ b/src/main/java/ee/carlrobert/codegpt/client/RequestHandler.java @@ -10,6 +10,7 @@ import okhttp3.sse.EventSource; public class RequestHandler implements ActionListener { private final Conversation conversation; + private final StringBuilder messageBuilder = new StringBuilder(); private SwingWorker swingWorker; private EventSource eventSource; @@ -35,8 +36,10 @@ public class RequestHandler implements ActionListener { } protected void process(List chunks) { + message.setResponse(messageBuilder.toString()); for (String text : chunks) { - handleMessage(text); + messageBuilder.append(text); + handleMessage(text, messageBuilder.toString()); } } }; @@ -49,13 +52,17 @@ public class RequestHandler implements ActionListener { } private EventSource startCall(Message message, EventListener eventListener) { - var settings = SettingsState.getInstance(); - var requestProvider = new CompletionRequestProvider(message.getPrompt(), conversation); - if (settings.isChatCompletionOptionSelected) { - return ClientProvider.getChatCompletionClient().stream( - requestProvider.buildChatCompletionRequest(settings.chatCompletionBaseModel), eventListener); + try { + var settings = SettingsState.getInstance(); + var requestProvider = new CompletionRequestProvider(message.getPrompt(), conversation); + if (settings.isChatCompletionOptionSelected) { + return ClientProvider.getChatCompletionClient().stream( + requestProvider.buildChatCompletionRequest(settings.chatCompletionBaseModel), eventListener); + } + return ClientProvider.getTextCompletionClient().stream( + requestProvider.buildTextCompletionRequest(settings.textCompletionBaseModel), eventListener); + } finally { + conversation.addMessage(message); } - return ClientProvider.getTextCompletionClient().stream( - requestProvider.buildTextCompletionRequest(settings.textCompletionBaseModel), eventListener); } } diff --git a/src/main/java/ee/carlrobert/codegpt/state/settings/SettingsConfigurable.java b/src/main/java/ee/carlrobert/codegpt/state/settings/SettingsConfigurable.java index 8fbd66ac..d1700fe6 100644 --- a/src/main/java/ee/carlrobert/codegpt/state/settings/SettingsConfigurable.java +++ b/src/main/java/ee/carlrobert/codegpt/state/settings/SettingsConfigurable.java @@ -4,7 +4,7 @@ import com.intellij.openapi.options.Configurable; import com.intellij.openapi.project.ProjectUtil; import ee.carlrobert.codegpt.state.conversations.ConversationsState; import ee.carlrobert.codegpt.toolwindow.chat.ChatContentManagerService; -import ee.carlrobert.codegpt.toolwindow.chat.ChatToolWindowTabPanel; +import ee.carlrobert.codegpt.toolwindow.chat.ToolWindowTabPanelFactory; import javax.swing.JComponent; import org.jetbrains.annotations.Nls; import org.jetbrains.annotations.Nullable; @@ -49,7 +49,7 @@ public class SettingsConfigurable implements Configurable { .tryFindChatTabbedPane() .ifPresent(tabbedPane -> { tabbedPane.clearAll(); - var tabPanel = new ChatToolWindowTabPanel(project); + var tabPanel = ToolWindowTabPanelFactory.getTabPanel(project); tabPanel.displayLandingView(); tabbedPane.addNewTab(tabPanel); }); diff --git a/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/BrowserContentManager.java b/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/BrowserContentManager.java new file mode 100644 index 00000000..1d9ec6a8 --- /dev/null +++ b/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/BrowserContentManager.java @@ -0,0 +1,189 @@ +package ee.carlrobert.codegpt.toolwindow.chat; + +import com.intellij.util.ui.UIUtil; +import com.vladsch.flexmark.html.HtmlRenderer; +import com.vladsch.flexmark.parser.Parser; +import com.vladsch.flexmark.util.data.MutableDataSet; +import java.util.UUID; +import org.cef.browser.CefBrowser; + +// TODO: Find a better way to work with JS +public class BrowserContentManager { + + private final CefBrowser browser; + + BrowserContentManager(CefBrowser browser) { + this.browser = browser; + } + + // language=javascript + public void displayUserMessage(String accountName, String prompt) { + clearLandingViewIfPresent(); + executeIIFE( + "const wrapper = document.createElement('div');" + + "wrapper.classList.add('user-message');" + + + "const nameLabel = document.createElement('p');" + + "nameLabel.textContent = '" + accountName + "';" + + "wrapper.appendChild(nameLabel);" + + + "const message = document.createElement('div');" + + "message.innerHTML = \"" + convertToHtml(prompt) + "\";" + + "wrapper.appendChild(message);" + + + "document.body.appendChild(wrapper);" + + "Prism.highlightAll();" + ); + } + + // language=javascript + public void displayResponse(UUID responseId, boolean animate) { + executeIIFE( + "const wrapper = document.createElement('div');" + + "wrapper.classList.add('response');" + + + "const iconLabelContainer = document.createElement('p');" + + + "const img = new Image();" + + "img.src = 'data:image/svg+xml;base64," + getBase64CodeGPTIcon() + "';" + + "const svgString = atob('" + getBase64CodeGPTIcon() + "');" + + "const svgDoc = new DOMParser().parseFromString(svgString, 'image/svg+xml');" + + "const svg = svgDoc.documentElement;" + + "svg.classList.add('svg-icon');" + + (animate ? "svg.style.animation = 'roll 2.4s infinite linear';" : "") + + "iconLabelContainer.appendChild(svg);" + + + "const label = document.createElement('strong');" + + "label.textContent = 'CodeGPT';" + + "iconLabelContainer.appendChild(label);" + + + "wrapper.appendChild(iconLabelContainer);" + + + "const responseWrapper = document.createElement('div');" + + "responseWrapper.setAttribute('id', '" + responseId + "');" + + "responseWrapper.innerHTML = \"

\";" + // display invisible character so that the height doesn't jump on the first response + "wrapper.appendChild(responseWrapper);" + + "document.body.appendChild(wrapper);" + ); + scrollToBottom(); + } + + // language=javascript + public void replaceResponseContent(UUID responseId, String message) { + executeIIFE( + "const responseWrapper = document.getElementById('" + responseId + "');" + + "responseWrapper.innerHTML = \"" + convertToHtml(message) + "\";" + + "Prism.highlightAll();" + ); + scrollToBottom(); + } + + // language=javascript + public void displayLandingView() { + executeIIFE( + "const wrapper = document.createElement('div');" + + "wrapper.setAttribute('id', 'landing-view');" + + "const buttonsWrapper = document.createElement('div');" + + "buttonsWrapper.innerHTML = " + + "'

Examples

" + + "\"How do I make an HTTP request in Javascript?\"" + + "\"What is the difference between px, dip, dp, and sp?\"" + + "\"How do I undo the most recent local commits in Git?\"" + + "\"What is the difference between stack and heap?\"';" + + "wrapper.appendChild(buttonsWrapper);" + + "document.body.appendChild(wrapper);" + ); + } + + // language=javascript + public void displayErrorMessage(UUID responseId, String errorMessage) { + executeIIFE( + "const errorLabel = document.createElement('p');" + + "errorLabel.textContent = '" + convertToHtml(errorMessage) + "';" + + "const responseWrapper = document.getElementById('" + responseId + "');" + + "responseWrapper.appendChild(errorLabel);" + ); + } + + // language=javascript + public void displayMissingCredential(UUID responseId) { + executeIIFE( + "const responseWrapper = document.getElementById('" + responseId + "');" + + // TODO: Add anchor link for opening the settings panel + "responseWrapper.innerHTML = \"

API key not provided.

\";" + ); + } + + // language=javascript + public void stopTyping() { + executeIIFE( + "const icons = Array.from(document.getElementsByClassName('svg-icon'));" + + "for (const icon of icons) {" + + " icon.style = null;" + + "}" + ); + } + + // language=javascript + public void clearLandingViewIfPresent() { + executeIIFE("document.getElementById(\"landing-view\")?.remove();"); + } + + // language=javascript + public void clearResponse(UUID responseId) { + executeIIFE( + "var responseWrapper = document.getElementById('" + responseId + "');" + + "responseWrapper.innerHTML = \"

\";" + ); + } + + // language=javascript + public void animateSvg(UUID responseId) { + executeIIFE( + "var response = document.getElementById('" + responseId + "');" + + "var svg = response.parentElement.getElementsByClassName('svg-icon')[0];" + + "svg.style.animation = 'roll 2.4s infinite linear';" + ); + } + + // language=javascript + private void scrollToBottom() { + executeJS( + "window.scrollTo({" + + " top: document.body.scrollHeight," + + " behavior: 'smooth'" + + "});" + ); + } + + // language=javascript + private void executeIIFE(String code) { + executeJS("(function () {" + code + "})();"); + } + + private void executeJS(String code) { + browser.executeJavaScript(code, null, 0); + } + + private String convertToHtml(String message) { + MutableDataSet options = new MutableDataSet(); + var document = Parser.builder(options).build().parse(message); + var html = HtmlRenderer.builder(options) + .build() + .render(document); + return normalizeHtml(html); + } + + private String normalizeHtml(String text) { + return text.replace("'", "\\'") + .replace("\n", "\\n") + .replace("\"", "\\\""); + } + + private String getBase64CodeGPTIcon() { + if (UIUtil.isUnderDarcula()) { + return "PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB2aWV3Qm94PSIwLDAsMjU2LDI1NiIgd2lkdGg9IjIwcHgiIGhlaWdodD0iMjBweCIKICAgIGZpbGwtcnVsZT0ibm9uemVybyI+CiAgICA8ZGVmcz4KICAgICAgICA8bGluZWFyR3JhZGllbnQgeDE9IjMyIiB5MT0iNyIgeDI9IjMyIiB5Mj0iNTgiIGdyYWRpZW50VW5pdHM9InVzZXJTcGFjZU9uVXNlIiBpZD0iY29sb3ItMSI+CiAgICAgICAgICAgIDxzdG9wIG9mZnNldD0iMCIgc3RvcC1jb2xvcj0iI2ZmZmZmZiI+PC9zdG9wPgogICAgICAgICAgICA8c3RvcCBvZmZzZXQ9IjAuNjk5IiBzdG9wLWNvbG9yPSIjZmZmZmZmIj48L3N0b3A+CiAgICAgICAgPC9saW5lYXJHcmFkaWVudD4KICAgICAgICA8bGluZWFyR3JhZGllbnQgeDE9IjMyIiB5MT0iMC44NzIiIHgyPSIzMiIgeTI9IjYyLjY3OSIgZ3JhZGllbnRVbml0cz0idXNlclNwYWNlT25Vc2UiIGlkPSJjb2xvci0yIj4KICAgICAgICAgICAgPHN0b3Agb2Zmc2V0PSIwIiBzdG9wLWNvbG9yPSIjMDAwMDAwIj48L3N0b3A+CiAgICAgICAgICAgIDxzdG9wIG9mZnNldD0iMSIgc3RvcC1jb2xvcj0iIzAwMDAwMCI+PC9zdG9wPgogICAgICAgIDwvbGluZWFyR3JhZGllbnQ+CiAgICA8L2RlZnM+CiAgICA8ZyB0cmFuc2Zvcm09InRyYW5zbGF0ZSgtMTYsLTE2KSBzY2FsZSgxLjEyNSwxLjEyNSkiPgogICAgICAgIDxnIGZpbGw9IiNmZmZmZmYiIGZpbGwtcnVsZT0ibm9uemVybyIgc3Ryb2tlPSIjZmZmZmZmIiBzdHJva2Utd2lkdGg9IjQiIHN0cm9rZS1saW5lY2FwPSJidXR0IiBzdHJva2UtbGluZWpvaW49InJvdW5kIiBzdHJva2UtbWl0ZXJsaW1pdD0iMTAiCiAgICAgICAgICAgIHN0cm9rZS1kYXNoYXJyYXk9IiIgc3Ryb2tlLWRhc2hvZmZzZXQ9IjAiIGZvbnQtZmFtaWx5PSJub25lIiBmb250LXdlaWdodD0ibm9uZSIgZm9udC1zaXplPSJub25lIiB0ZXh0LWFuY2hvcj0ibm9uZSIKICAgICAgICAgICAgc3R5bGU9Im1peC1ibGVuZC1tb2RlOiBub3JtYWwiPgogICAgICAgICAgICA8cGF0aCB0cmFuc2Zvcm09InNjYWxlKDQsNCkiCiAgICAgICAgICAgICAgICBkPSJNNTYuOTYsMzUuNzdjMCwzLjI2Njc3IC0xLjI0Nzk4LDYuMjM1MDYgLTMuMzAyNzYsOC40NDkzMmMtMS45OTQyOSwyLjE1MTY0IC00Ljc1MDcyLDMuNTkxMTkgLTcuODY3NTcsMy45MDIyMmMtMC44ODIwOCw0LjE3OTkgLTMuODU4MDIsNy41NTAxMiAtNy43MDY2Nyw5LjA0NDE0Yy0xLjM4OTE3LDAuNTM5NDQgLTIuODkxOCwwLjgzNDMyIC00LjQ1MDAxLDAuODM0MzJjLTAuMDAwNSwwIC0wLjAwMTAxLDAgLTAuMDAxNTEsMGMtMC4wMDA1LDAgLTAuMDAwOTksMCAtMC4wMDE0OSwwYy0zLjcsMCAtNy4xMywtMS42MSAtOS41MSwtNC40NGMtMC4xNzM1NSwwLjAyMDgzIC0wLjMzOTg4LDAuMDM2ODMgLTAuNTAxNDgsMC4wNDg4NmMtMC4zMDUxMywwLjAyNDY1IC0wLjU5MTAzLDAuMDM2MTQgLTAuODczNTIsMC4wMzYxNGMtMy42NzIxOCwwIC02Ljk3NzczLC0xLjYwMDUzIC05LjI1NTksLTQuMTQwMzljLTEuOTc1NDcsLTIuMjAwNzYgLTMuMTc5MSwtNS4xMDg1NiAtMy4xNzkxLC04LjI5NDYxYzAsLTEuMzU5OTIgMC4yMTk5NywtMi42OTk4NCAwLjY1OTkyLC0zLjk5OTc3Yy0wLjE0NDAzLC0wLjEzNTUgLTAuMjg0NDksLTAuMjc0MDIgLTAuNDIxMywtMC40MTU0MmMtMi4yMzg2NSwtMi4zMTIyIC0zLjUwODYzLC01LjQwMzU3IC0zLjUwODYzLC04LjY1NDgxYzAsLTYuMDkgNC4zOSwtMTEuMjQgMTAuMzIsLTEyLjI1YzEuMjI3OTksLTQuMTQ4MzkgNC40Njk4LC03LjI3NDM4IDguNTAxMTUsLTguNDE3MDdjMS4wODkxNywtMC4zMDg5NCAyLjIzNjE0LC0wLjQ3MjkzIDMuNDE2ODUsLTAuNDcyOTNjMC4wMDAzMywwIDAuMDAwNjcsMCAwLjAwMSwwYzAuMDAwMzMsMCAwLjAwMDY2LDAgMC4wMDEsMGMwLjI5ODMzLDAgMC41OTQ3OSwwLjAxMDQgMC44ODg5OSwwLjAzMWMyLjI5NDA1LDAuMTYwMzEgNC40NDcwNywwLjkzODI5IDYuMjc0MTMsMi4yMzk3NGMwLjkxMDYzLDAuNjQ3NTcgMS43NDA0NCwxLjQyNDM5IDIuNDY2MiwyLjMxODQxYzAuMDU2NTksLTAuMDExMDYgMC4xMTMxNCwtMC4wMjE3MyAwLjE2OTYzLC0wLjAzMjAxYzAuMjQ2ODgsLTAuMDQ2MDMgMC40OTM2MywtMC4wODM0MSAwLjczOTYzLC0wLjExMjc0YzAuNDg1MDYsLTAuMDU5NDUgMC45NjQ3MywtMC4wODk0MSAxLjQzNjQzLC0wLjA4OTQxYzAuMTE4NzYsMCAwLjIzNzE1LDAuMDAxNjcgMC4zNTUxMiwwLjAwNWMzLjUyOTgsMC4wOTk1MSA2LjY5NjI5LDEuNjc3NjcgOC45MDA3OCw0LjEzNTM5YzEuOTc1NDcsMi4yMDA3NiAzLjE3OTEsNS4xMDg1NiAzLjE3OTEsOC4yOTQ2MWMwLDAuNjc4NjEgLTAuMDU3MzgsMS4zNTcyMyAtMC4xNzIxNCwyLjAzMzIyYy0wLjA2MzcxLDAuMzc4ODQgLTAuMTQ1MDgsMC43NTY0IC0wLjI0Mzg2LDEuMTMxNzhjMS4xMjkxOCwxLjEyMTAxIDIuMDIzODUsMi40Mzc2NyAyLjY0ODMsMy44NzAwM2MwLjY3NjQsMS41NDUyNCAxLjAzNzcsMy4yMjYyMyAxLjAzNzcsNC45NDQ5N3oiCiAgICAgICAgICAgICAgICBpZD0ic3Ryb2tlTWFpblNWRyI+PC9wYXRoPgogICAgICAgIDwvZz4KICAgICAgICA8ZyBmaWxsPSJub25lIiBmaWxsLXJ1bGU9Im5vbnplcm8iIHN0cm9rZT0ibm9uZSIgc3Ryb2tlLXdpZHRoPSIxIiBzdHJva2UtbGluZWNhcD0iYnV0dCIgc3Ryb2tlLWxpbmVqb2luPSJtaXRlciIgc3Ryb2tlLW1pdGVybGltaXQ9IjEwIgogICAgICAgICAgICBzdHJva2UtZGFzaGFycmF5PSIiIHN0cm9rZS1kYXNob2Zmc2V0PSIwIiBmb250LWZhbWlseT0ibm9uZSIgZm9udC13ZWlnaHQ9Im5vbmUiIGZvbnQtc2l6ZT0ibm9uZSIgdGV4dC1hbmNob3I9Im5vbmUiCiAgICAgICAgICAgIHN0eWxlPSJtaXgtYmxlbmQtbW9kZTogbm9ybWFsIj4KICAgICAgICAgICAgPGcgdHJhbnNmb3JtPSJzY2FsZSg0LDQpIj4KICAgICAgICAgICAgICAgIDxwYXRoCiAgICAgICAgICAgICAgICAgICAgZD0iTTUzLjI3LDI2Ljk2YzAuMjgsLTEuMDUgMC40MiwtMi4xMSAwLjQyLC0zLjE3YzAsLTYuODYgLTUuNTgsLTEyLjQzIC0xMi40MywtMTIuNDNjLTAuNzcsMCAtMS41NiwwLjA3IC0yLjM1LDAuMjNjLTIuMzcsLTIuOTIgLTUuODUsLTQuNTkgLTkuNjMsLTQuNTljLTUuNTUsMCAtMTAuMzYsMy42MiAtMTEuOTIsOC44OWMtNS45MywxLjAxIC0xMC4zMiw2LjE2IC0xMC4zMiwxMi4yNWMwLDMuNDUgMS40Myw2LjcyIDMuOTMsOS4wN2MtMC40NCwxLjMgLTAuNjYsMi42NCAtMC42Niw0YzAsNi44NiA1LjU4LDEyLjQzIDEyLjQzLDEyLjQzYzAuNDQsMCAwLjg4LC0wLjAyIDEuMzgsLTAuMDhjMi4zOCwyLjgzIDUuODEsNC40NCA5LjUxLDQuNDRjNS44OCwwIDEwLjk2LC00LjE5IDEyLjE2LC05Ljg4YzYuMzIsLTAuNjMgMTEuMTcsLTUuOTEgMTEuMTcsLTEyLjM1YzAsLTMuMzEgLTEuMzQsLTYuNDggLTMuNjksLTguODF6TTM4LjI1LDM1Ljg4bC02LjYzLDRsLTYuNSwtNHYtNy4yNmw2LjYzLC0zLjg3bDYuNjMsMy43NXoiCiAgICAgICAgICAgICAgICAgICAgZmlsbD0idXJsKCNjb2xvci0xKSI+PC9wYXRoPgogICAgICAgICAgICAgICAgPHBhdGgKICAgICAgICAgICAgICAgICAgICBkPSJNNTMuMjc0LDI2Ljk1NWMwLjI3NSwtMS4wNDUgMC40MTUsLTIuMTA3IDAuNDE1LC0zLjE2NmMwLC02Ljg1NSAtNS41NzgsLTEyLjQzNCAtMTIuNDM0LC0xMi40MzRjLTAuNzY2LDAgLTEuNTUzLDAuMDc5IC0yLjM1LDAuMjM1Yy0yLjM2OSwtMi45MjUgLTUuODQzLC00LjU5IC05LjYyNywtNC41OWMtNS41NDksMCAtMTAuMzUzLDMuNjIyIC0xMS45MTMsOC44OTFjLTUuOTMsMS4wMTIgLTEwLjMyLDYuMTYzIC0xMC4zMiwxMi4yNTRjMCwzLjQ0OCAxLjQyNCw2LjcxNSAzLjkzLDkuMDdjLTAuNDQsMS4yOTkgLTAuNjY0LDIuNjQgLTAuNjY0LDMuOTk2YzAsNi44NTUgNS41NzgsMTIuNDM0IDEyLjQzNCwxMi40MzRjMC40MzMsMCAwLjg3NCwtMC4wMjcgMS4zOCwtMC4wODdjMi4zNzYsMi44MzEgNS44MDksNC40NDIgOS41MDgsNC40NDJjNS44NzUsMCAxMC45NiwtNC4xOTIgMTIuMTUyLC05Ljg3OGM2LjMyNywtMC42MjkgMTEuMTcsLTUuOTA4IDExLjE3LC0xMi4zNTVjMC4wMDEsLTMuMzA0IC0xLjMzMywtNi40ODEgLTMuNjgxLC04LjgxMnpNNTEuNjg5LDIzLjc4OWMwLDAuNjQ2IC0wLjA3LDEuMjkzIC0wLjE5MywxLjkzN2wtMTIuMjkzLC03LjE4NWwtMTMuMTQ2LDcuOTkxdi00LjkxbDEyLjgxNCwtNy45NzJjMC44MTMsLTAuMTkxIDEuNjE1LC0wLjI5NSAyLjM4MywtMC4yOTVjNS43NTQsMCAxMC40MzUsNC42ODEgMTAuNDM1LDEwLjQzNHpNMzcuMzk3LDM1LjE3MWwtNS41NjMsMy4zMTZsLTUuNzc2LC0zLjMwM3YtNi4zMTFsNS40NjUsLTMuMzIybC0wLjAzMSwwLjA1Mmw1LjkwNSwzLjQ4ek0yOS4yNzgsOWMyLjk5NywwIDUuNzU1LDEuMjUxIDcuNzI4LDMuNDU3bC0xMi45NDgsOC4wNTR2MTMuNTI5bC00Ljg5OCwtMi44MDF2LTE0LjMxN2MxLjE1OSwtNC42NjggNS4zMDIsLTcuOTIyIDEwLjExOCwtNy45MjJ6TTkuMDQ0LDI4LjE0NWMwLC00LjkyMyAzLjQxOSwtOS4xMDkgOC4xMTYsLTEwLjE2OXYxNC40MjRsMTIuNzAxLDcuMjY0bC01LjIyNywzLjExNWwtMTEuODk3LC02LjY3NGMtMi4zNDUsLTEuOTk4IC0zLjY5MywtNC44OTQgLTMuNjkzLC03Ljk2ek0xMi4zMTEsNDEuMjExYzAsLTAuOTU1IDAuMTM4LC0xLjkwMiAwLjQsLTIuODI4bDExLjk1NCw2LjcwNmwxMi43MzIsLTcuNTg4djYuMjdsLTEzLjE3Miw3Ljc1NGMtMC41NywwLjA3OCAtMS4wNDMsMC4xMiAtMS40OCwwLjEyYy01Ljc1MywwIC0xMC40MzQsLTQuNjgxIC0xMC40MzQsLTEwLjQzNHpNMzMuNjMzLDU2Yy0yLjg4NiwwIC01LjU3OCwtMS4xNzUgLTcuNTQ2LC0zLjI1MmwxMy4zMSwtNy44MzV2LTE0LjY1Mmw0LjUzOSwyLjY3NXYxNC4xNTRjLTAuNzQ0LDUuMDgzIC01LjE2Myw4LjkxIC0xMC4zMDMsOC45MXpNNDUuOTM2LDQ2LjA5MXYtMTQuMjk4bC02LjUzOSwtMy44NTN2LTAuMDY4aC0wLjExNWwtNS44NzksLTMuNDY1bDUuODIxLC0zLjUzOGwxMi4zMDksNy4xOTVjMi4xNzQsMS45ODEgMy40MjIsNC43ODIgMy40MjIsNy43MDNjMC4wMDEsNS4yODggLTMuODg1LDkuNjM5IC05LjAxOSwxMC4zMjR6IgogICAgICAgICAgICAgICAgICAgIGZpbGw9InVybCgjY29sb3ItMikiPjwvcGF0aD4KICAgICAgICAgICAgPC9nPgogICAgICAgIDwvZz4KICAgIDwvZz4KPC9zdmc+Cg=="; + } + return "PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB2aWV3Qm94PSIwLDAsMjU2LDI1NiIgd2lkdGg9IjIwcHgiIGhlaWdodD0iMjBweCIgZmlsbC1ydWxlPSJub256ZXJvIj48ZGVmcz48bGluZWFyR3JhZGllbnQgeDE9IjMyIiB5MT0iNyIgeDI9IjMyIiB5Mj0iNTgiIGdyYWRpZW50VW5pdHM9InVzZXJTcGFjZU9uVXNlIiBpZD0iY29sb3ItMSI+PHN0b3Agb2Zmc2V0PSIwIiBzdG9wLWNvbG9yPSIjMDAwMDAwIj48L3N0b3A+PHN0b3Agb2Zmc2V0PSIwLjY5OSIgc3RvcC1jb2xvcj0iIzAwMDAwMCI+PC9zdG9wPjwvbGluZWFyR3JhZGllbnQ+PGxpbmVhckdyYWRpZW50IHgxPSIzMiIgeTE9IjAuODcyIiB4Mj0iMzIiIHkyPSI2Mi42NzkiIGdyYWRpZW50VW5pdHM9InVzZXJTcGFjZU9uVXNlIiBpZD0iY29sb3ItMiI+PHN0b3Agb2Zmc2V0PSIwIiBzdG9wLWNvbG9yPSIjZmZmZmZmIj48L3N0b3A+PHN0b3Agb2Zmc2V0PSIxIiBzdG9wLWNvbG9yPSIjZmZmZmZmIj48L3N0b3A+PC9saW5lYXJHcmFkaWVudD48L2RlZnM+PGcgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoLTE2LC0xNikgc2NhbGUoMS4xMjUsMS4xMjUpIj48ZyBmaWxsPSIjMDAwMDAwIiBmaWxsLXJ1bGU9Im5vbnplcm8iIHN0cm9rZT0iIzAwMDAwMCIgc3Ryb2tlLXdpZHRoPSI0IiBzdHJva2UtbGluZWNhcD0iYnV0dCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIgc3Ryb2tlLW1pdGVybGltaXQ9IjEwIiBzdHJva2UtZGFzaGFycmF5PSIiIHN0cm9rZS1kYXNob2Zmc2V0PSIwIiBmb250LWZhbWlseT0ibm9uZSIgZm9udC13ZWlnaHQ9Im5vbmUiIGZvbnQtc2l6ZT0ibm9uZSIgdGV4dC1hbmNob3I9Im5vbmUiIHN0eWxlPSJtaXgtYmxlbmQtbW9kZTogbm9ybWFsIj48cGF0aCB0cmFuc2Zvcm09InNjYWxlKDQsNCkiIGQ9Ik01Ni45NiwzNS43N2MwLDMuMjY2NzcgLTEuMjQ3OTgsNi4yMzUwNiAtMy4zMDI3Niw4LjQ0OTMyYy0xLjk5NDI5LDIuMTUxNjQgLTQuNzUwNzIsMy41OTExOSAtNy44Njc1NywzLjkwMjIyYy0wLjg4MjA4LDQuMTc5OSAtMy44NTgwMiw3LjU1MDEyIC03LjcwNjY3LDkuMDQ0MTRjLTEuMzg5MTcsMC41Mzk0NCAtMi44OTE4LDAuODM0MzIgLTQuNDUwMDEsMC44MzQzMmMtMC4wMDA1LDAgLTAuMDAxMDEsMCAtMC4wMDE1MSwwYy0wLjAwMDUsMCAtMC4wMDA5OSwwIC0wLjAwMTQ5LDBjLTMuNywwIC03LjEzLC0xLjYxIC05LjUxLC00LjQ0Yy0wLjE3MzU1LDAuMDIwODMgLTAuMzM5ODgsMC4wMzY4MyAtMC41MDE0OCwwLjA0ODg2Yy0wLjMwNTEzLDAuMDI0NjUgLTAuNTkxMDMsMC4wMzYxNCAtMC44NzM1MiwwLjAzNjE0Yy0zLjY3MjE4LDAgLTYuOTc3NzMsLTEuNjAwNTMgLTkuMjU1OSwtNC4xNDAzOWMtMS45NzU0NywtMi4yMDA3NiAtMy4xNzkxLC01LjEwODU2IC0zLjE3OTEsLTguMjk0NjFjMCwtMS4zNTk5MiAwLjIxOTk3LC0yLjY5OTg0IDAuNjU5OTIsLTMuOTk5NzdjLTAuMTQ0MDMsLTAuMTM1NSAtMC4yODQ0OSwtMC4yNzQwMiAtMC40MjEzLC0wLjQxNTQyYy0yLjIzODY1LC0yLjMxMjIgLTMuNTA4NjMsLTUuNDAzNTcgLTMuNTA4NjMsLTguNjU0ODFjMCwtNi4wOSA0LjM5LC0xMS4yNCAxMC4zMiwtMTIuMjVjMS4yMjc5OSwtNC4xNDgzOSA0LjQ2OTgsLTcuMjc0MzggOC41MDExNSwtOC40MTcwN2MxLjA4OTE3LC0wLjMwODk0IDIuMjM2MTQsLTAuNDcyOTMgMy40MTY4NSwtMC40NzI5M2MwLjAwMDMzLDAgMC4wMDA2NywwIDAuMDAxLDBjMC4wMDAzMywwIDAuMDAwNjYsMCAwLjAwMSwwYzAuMjk4MzMsMCAwLjU5NDc5LDAuMDEwNCAwLjg4ODk5LDAuMDMxYzIuMjk0MDUsMC4xNjAzMSA0LjQ0NzA3LDAuOTM4MjkgNi4yNzQxMywyLjIzOTc0YzAuOTEwNjMsMC42NDc1NyAxLjc0MDQ0LDEuNDI0MzkgMi40NjYyLDIuMzE4NDFjMC4wNTY1OSwtMC4wMTEwNiAwLjExMzE0LC0wLjAyMTczIDAuMTY5NjMsLTAuMDMyMDFjMC4yNDY4OCwtMC4wNDYwMyAwLjQ5MzYzLC0wLjA4MzQxIDAuNzM5NjMsLTAuMTEyNzRjMC40ODUwNiwtMC4wNTk0NSAwLjk2NDczLC0wLjA4OTQxIDEuNDM2NDMsLTAuMDg5NDFjMC4xMTg3NiwwIDAuMjM3MTUsMC4wMDE2NyAwLjM1NTEyLDAuMDA1YzMuNTI5OCwwLjA5OTUxIDYuNjk2MjksMS42Nzc2NyA4LjkwMDc4LDQuMTM1MzljMS45NzU0NywyLjIwMDc2IDMuMTc5MSw1LjEwODU2IDMuMTc5MSw4LjI5NDYxYzAsMC42Nzg2MSAtMC4wNTczOCwxLjM1NzIzIC0wLjE3MjE0LDIuMDMzMjJjLTAuMDYzNzEsMC4zNzg4NCAtMC4xNDUwOCwwLjc1NjQgLTAuMjQzODYsMS4xMzE3OGMxLjEyOTE4LDEuMTIxMDEgMi4wMjM4NSwyLjQzNzY3IDIuNjQ4MywzLjg3MDAzYzAuNjc2NCwxLjU0NTI0IDEuMDM3NywzLjIyNjIzIDEuMDM3Nyw0Ljk0NDk3eiIgaWQ9InN0cm9rZU1haW5TVkciPjwvcGF0aD48L2c+PGcgZmlsbD0ibm9uZSIgZmlsbC1ydWxlPSJub256ZXJvIiBzdHJva2U9Im5vbmUiIHN0cm9rZS13aWR0aD0iMSIgc3Ryb2tlLWxpbmVjYXA9ImJ1dHQiIHN0cm9rZS1saW5lam9pbj0ibWl0ZXIiIHN0cm9rZS1taXRlcmxpbWl0PSIxMCIgc3Ryb2tlLWRhc2hhcnJheT0iIiBzdHJva2UtZGFzaG9mZnNldD0iMCIgZm9udC1mYW1pbHk9Im5vbmUiIGZvbnQtd2VpZ2h0PSJub25lIiBmb250LXNpemU9Im5vbmUiIHRleHQtYW5jaG9yPSJub25lIiBzdHlsZT0ibWl4LWJsZW5kLW1vZGU6IG5vcm1hbCI+PGcgdHJhbnNmb3JtPSJzY2FsZSg0LDQpIj48cGF0aCBkPSJNNTMuMjcsMjYuOTZjMC4yOCwtMS4wNSAwLjQyLC0yLjExIDAuNDIsLTMuMTdjMCwtNi44NiAtNS41OCwtMTIuNDMgLTEyLjQzLC0xMi40M2MtMC43NywwIC0xLjU2LDAuMDcgLTIuMzUsMC4yM2MtMi4zNywtMi45MiAtNS44NSwtNC41OSAtOS42MywtNC41OWMtNS41NSwwIC0xMC4zNiwzLjYyIC0xMS45Miw4Ljg5Yy01LjkzLDEuMDEgLTEwLjMyLDYuMTYgLTEwLjMyLDEyLjI1YzAsMy40NSAxLjQzLDYuNzIgMy45Myw5LjA3Yy0wLjQ0LDEuMyAtMC42NiwyLjY0IC0wLjY2LDRjMCw2Ljg2IDUuNTgsMTIuNDMgMTIuNDMsMTIuNDNjMC40NCwwIDAuODgsLTAuMDIgMS4zOCwtMC4wOGMyLjM4LDIuODMgNS44MSw0LjQ0IDkuNTEsNC40NGM1Ljg4LDAgMTAuOTYsLTQuMTkgMTIuMTYsLTkuODhjNi4zMiwtMC42MyAxMS4xNywtNS45MSAxMS4xNywtMTIuMzVjMCwtMy4zMSAtMS4zNCwtNi40OCAtMy42OSwtOC44MXpNMzguMjUsMzUuODhsLTYuNjMsNGwtNi41LC00di03LjI2bDYuNjMsLTMuODdsNi42MywzLjc1eiIgZmlsbD0idXJsKCNjb2xvci0xKSI+PC9wYXRoPjxwYXRoIGQ9Ik01My4yNzQsMjYuOTU1YzAuMjc1LC0xLjA0NSAwLjQxNSwtMi4xMDcgMC40MTUsLTMuMTY2YzAsLTYuODU1IC01LjU3OCwtMTIuNDM0IC0xMi40MzQsLTEyLjQzNGMtMC43NjYsMCAtMS41NTMsMC4wNzkgLTIuMzUsMC4yMzVjLTIuMzY5LC0yLjkyNSAtNS44NDMsLTQuNTkgLTkuNjI3LC00LjU5Yy01LjU0OSwwIC0xMC4zNTMsMy42MjIgLTExLjkxMyw4Ljg5MWMtNS45MywxLjAxMiAtMTAuMzIsNi4xNjMgLTEwLjMyLDEyLjI1NGMwLDMuNDQ4IDEuNDI0LDYuNzE1IDMuOTMsOS4wN2MtMC40NCwxLjI5OSAtMC42NjQsMi42NCAtMC42NjQsMy45OTZjMCw2Ljg1NSA1LjU3OCwxMi40MzQgMTIuNDM0LDEyLjQzNGMwLjQzMywwIDAuODc0LC0wLjAyNyAxLjM4LC0wLjA4N2MyLjM3NiwyLjgzMSA1LjgwOSw0LjQ0MiA5LjUwOCw0LjQ0MmM1Ljg3NSwwIDEwLjk2LC00LjE5MiAxMi4xNTIsLTkuODc4YzYuMzI3LC0wLjYyOSAxMS4xNywtNS45MDggMTEuMTcsLTEyLjM1NWMwLjAwMSwtMy4zMDQgLTEuMzMzLC02LjQ4MSAtMy42ODEsLTguODEyek01MS42ODksMjMuNzg5YzAsMC42NDYgLTAuMDcsMS4yOTMgLTAuMTkzLDEuOTM3bC0xMi4yOTMsLTcuMTg1bC0xMy4xNDYsNy45OTF2LTQuOTFsMTIuODE0LC03Ljk3MmMwLjgxMywtMC4xOTEgMS42MTUsLTAuMjk1IDIuMzgzLC0wLjI5NWM1Ljc1NCwwIDEwLjQzNSw0LjY4MSAxMC40MzUsMTAuNDM0ek0zNy4zOTcsMzUuMTcxbC01LjU2MywzLjMxNmwtNS43NzYsLTMuMzAzdi02LjMxMWw1LjQ2NSwtMy4zMjJsLTAuMDMxLDAuMDUybDUuOTA1LDMuNDh6TTI5LjI3OCw5YzIuOTk3LDAgNS43NTUsMS4yNTEgNy43MjgsMy40NTdsLTEyLjk0OCw4LjA1NHYxMy41MjlsLTQuODk4LC0yLjgwMXYtMTQuMzE3YzEuMTU5LC00LjY2OCA1LjMwMiwtNy45MjIgMTAuMTE4LC03LjkyMnpNOS4wNDQsMjguMTQ1YzAsLTQuOTIzIDMuNDE5LC05LjEwOSA4LjExNiwtMTAuMTY5djE0LjQyNGwxMi43MDEsNy4yNjRsLTUuMjI3LDMuMTE1bC0xMS44OTcsLTYuNjc0Yy0yLjM0NSwtMS45OTggLTMuNjkzLC00Ljg5NCAtMy42OTMsLTcuOTZ6TTEyLjMxMSw0MS4yMTFjMCwtMC45NTUgMC4xMzgsLTEuOTAyIDAuNCwtMi44MjhsMTEuOTU0LDYuNzA2bDEyLjczMiwtNy41ODh2Ni4yN2wtMTMuMTcyLDcuNzU0Yy0wLjU3LDAuMDc4IC0xLjA0MywwLjEyIC0xLjQ4LDAuMTJjLTUuNzUzLDAgLTEwLjQzNCwtNC42ODEgLTEwLjQzNCwtMTAuNDM0ek0zMy42MzMsNTZjLTIuODg2LDAgLTUuNTc4LC0xLjE3NSAtNy41NDYsLTMuMjUybDEzLjMxLC03LjgzNXYtMTQuNjUybDQuNTM5LDIuNjc1djE0LjE1NGMtMC43NDQsNS4wODMgLTUuMTYzLDguOTEgLTEwLjMwMyw4Ljkxek00NS45MzYsNDYuMDkxdi0xNC4yOThsLTYuNTM5LC0zLjg1M3YtMC4wNjhoLTAuMTE1bC01Ljg3OSwtMy40NjVsNS44MjEsLTMuNTM4bDEyLjMwOSw3LjE5NWMyLjE3NCwxLjk4MSAzLjQyMiw0Ljc4MiAzLjQyMiw3LjcwM2MwLjAwMSw1LjI4OCAtMy44ODUsOS42MzkgLTkuMDE5LDEwLjMyNHoiIGZpbGw9InVybCgjY29sb3ItMikiPjwvcGF0aD48L2c+PC9nPjwvZz48L3N2Zz4K"; + } +} diff --git a/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ChatContentManagerService.java b/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ChatContentManagerService.java index 461ca48f..7cc86e3a 100644 --- a/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ChatContentManagerService.java +++ b/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ChatContentManagerService.java @@ -19,11 +19,11 @@ public class ChatContentManagerService { this.project = project; } - public ChatToolWindowTabPanel createNewTabPanel() { + public ToolWindowTabPanel createNewTabPanel() { displayChatTab(); var tabbedPane = tryFindChatTabbedPane(); if (tabbedPane.isPresent()) { - var panel = new ChatToolWindowTabPanel(project); + var panel = ToolWindowTabPanelFactory.getTabPanel(project); tabbedPane.get().addNewTab(panel); return panel; } @@ -56,7 +56,7 @@ public class ChatContentManagerService { public void resetTabbedPane() { tryFindChatTabbedPane().ifPresent(tabbedPane -> { tabbedPane.clearAll(); - var tabPanel = new ChatToolWindowTabPanel(project); + var tabPanel = ToolWindowTabPanelFactory.getTabPanel(project); tabPanel.displayLandingView(); tabbedPane.addNewTab(tabPanel); }); diff --git a/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ChatTabbedPane.java b/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ChatTabbedPane.java index 6d0b15bb..49c03001 100644 --- a/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ChatTabbedPane.java +++ b/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ChatTabbedPane.java @@ -13,7 +13,7 @@ import javax.swing.JPanel; public class ChatTabbedPane extends JBTabbedPane { - private final Map activeTabMapping = new TreeMap<>((o1, o2) -> { + private final Map activeTabMapping = new TreeMap<>((o1, o2) -> { int n1 = Integer.parseInt(o1.replaceAll("\\D", "")); int n2 = Integer.parseInt(o2.replaceAll("\\D", "")); return Integer.compare(n1, n2); @@ -25,7 +25,7 @@ public class ChatTabbedPane extends JBTabbedPane { .ifPresent(conversation -> ConversationsState.getInstance().setCurrentConversation(conversation))); } - public void addNewTab(ChatToolWindowTabPanel toolWindowPanel) { + public void addNewTab(ToolWindowTabPanel toolWindowPanel) { var tabIndices = activeTabMapping.keySet().toArray(new String[0]); var nextIndex = 0; for (String title : tabIndices) { @@ -49,7 +49,10 @@ public class ChatTabbedPane extends JBTabbedPane { public Optional tryFindActiveConversationTitle(UUID conversationId) { return activeTabMapping.entrySet().stream() - .filter(entry -> conversationId.equals(entry.getValue().getConversation().getId())) + .filter(entry -> { + var panelConversation = entry.getValue().getConversation(); + return panelConversation != null && conversationId.equals(panelConversation.getId()); + }) .findFirst() .map(Map.Entry::getKey); } diff --git a/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ChatToolWindowPanel.java b/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ChatToolWindowPanel.java index 83c90f84..bb00c2ad 100644 --- a/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ChatToolWindowPanel.java +++ b/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ChatToolWindowPanel.java @@ -19,7 +19,7 @@ public class ChatToolWindowPanel extends SimpleToolWindowPanel { } private void initialize(Project project) { - var tabPanel = new ChatToolWindowTabPanel(project); + var tabPanel = ToolWindowTabPanelFactory.getTabPanel(project); var conversation = ConversationsState.getCurrentConversation(); if (conversation == null) { tabPanel.displayLandingView(); @@ -35,8 +35,7 @@ public class ChatToolWindowPanel extends SimpleToolWindowPanel { private ActionToolbar createActionToolbar(Project project, ChatTabbedPane tabbedPane) { var actionGroup = new DefaultActionGroup("TOOLBAR_ACTION_GROUP", false); actionGroup.add(new CreateNewConversationAction(() -> { - var panel = new ChatToolWindowTabPanel(project); - panel.setConversation(ConversationsState.getInstance().startConversation()); + var panel = ToolWindowTabPanelFactory.getTabPanel(project); panel.displayLandingView(); tabbedPane.addNewTab(panel); repaint(); @@ -45,14 +44,13 @@ public class ChatToolWindowPanel extends SimpleToolWindowPanel { actionGroup.add(new OpenInEditorAction()); actionGroup.addSeparator(); actionGroup.add(new UsageToolbarLabelAction()); - - // TODO: Data usage not enabled in stream mode https://community.openai.com/t/usage-info-in-api-responses/18862/11 + // actionGroup.addSeparator(); // actionGroup.add(new TokenToolbarLabelAction()); return ActionManager.getInstance().createActionToolbar("NAVIGATION_BAR_TOOLBAR", actionGroup, false); } - private ChatTabbedPane createTabbedPane(ChatToolWindowTabPanel tabPanel) { + private ChatTabbedPane createTabbedPane(ToolWindowTabPanel tabPanel) { var tabbedPane = new ChatTabbedPane(); tabbedPane.addNewTab(tabPanel); return tabbedPane; diff --git a/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ChatToolWindowTabHtmlPanel.form b/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ChatToolWindowTabHtmlPanel.form new file mode 100644 index 00000000..bc94dbb1 --- /dev/null +++ b/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ChatToolWindowTabHtmlPanel.form @@ -0,0 +1,45 @@ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
diff --git a/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ChatToolWindowTabHtmlPanel.java b/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ChatToolWindowTabHtmlPanel.java new file mode 100644 index 00000000..f10c6d1b --- /dev/null +++ b/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ChatToolWindowTabHtmlPanel.java @@ -0,0 +1,151 @@ +package ee.carlrobert.codegpt.toolwindow.chat; + +import com.intellij.openapi.project.Project; +import com.intellij.ui.JBColor; +import com.intellij.ui.components.JBScrollPane; +import com.intellij.util.ui.JBUI; +import ee.carlrobert.codegpt.client.RequestHandler; +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.state.settings.SettingsState; +import ee.carlrobert.codegpt.toolwindow.components.GenerateButton; +import ee.carlrobert.codegpt.toolwindow.components.TextArea; +import java.awt.BorderLayout; +import java.awt.Dimension; +import javax.swing.BorderFactory; +import javax.swing.JButton; +import javax.swing.JPanel; +import javax.swing.JScrollBar; +import javax.swing.JScrollPane; +import javax.swing.JTextArea; +import javax.swing.SwingUtilities; +import org.jetbrains.annotations.NotNull; + +public class ChatToolWindowTabHtmlPanel implements ToolWindowTabPanel { + + private final Project project; + private JPanel rootPanel; + private JTextArea textArea; + private JScrollPane textAreaScrollPane; + private JPanel contentPanel; + private JButton reloadResponseButton; + private MarkdownJCEFHtmlPanel markdownHtmlPanel; + private Conversation conversation; + + public ChatToolWindowTabHtmlPanel(@NotNull Project project) { + this.project = project; + } + + @Override + public JPanel getContent() { + return rootPanel; + } + + @Override + public Conversation getConversation() { + return conversation; + } + + @Override + public void displayLandingView() { + markdownHtmlPanel.displayLandingView(); + } + + @Override + public void displayConversation(Conversation conversation) { + this.conversation = conversation; + markdownHtmlPanel.displayConversation(conversation); + } + + @Override + public void startNewConversation(String prompt) { + markdownHtmlPanel.runWhenLoaded(() -> { + conversation = ConversationsState.getInstance().startConversation(); + startConversation(prompt, false); + }); + } + + @Override + public void startConversation(String prompt, boolean isRetry) { + markdownHtmlPanel.displayUserMessage(prompt); + var settings = SettingsState.getInstance(); + if (settings.apiKey.isEmpty()) { + markdownHtmlPanel.displayMissingCredential(); + } else { + SwingUtilities.invokeLater(() -> call(prompt, isRetry)); + } + } + + private void call(String prompt, boolean isRetry) { + if (conversation == null) { + conversation = ConversationsState.getInstance().startConversation(); + } + + var responseId = markdownHtmlPanel.prepareResponse(true, isRetry); + var conversationMessage = new Message(prompt); + var requestService = new RequestHandler(conversation) { + public void handleMessage(String message, String fullMessage) { + try { + markdownHtmlPanel.replaceHtml(responseId, fullMessage); + } catch (Exception e) { + markdownHtmlPanel.displayErrorMessage(); + throw new RuntimeException(e); + } + } + + public void handleComplete() { + stopGenerating(prompt); + markdownHtmlPanel.stopTyping(); + } + + public void handleError(String errorMessage) { + markdownHtmlPanel.displayErrorMessage(errorMessage); + } + }; + requestService.call(conversationMessage, isRetry); + displayReloadResponseButton(requestService::cancel); + } + + private void handleSubmit() { + startConversation(textArea.getText(), false); + textArea.setText(""); + } + + private void displayReloadResponseButton(Runnable onClick) { + var button = getReloadResponseButton(); + button.setVisible(true); + button.setMode(GenerateButton.Mode.STOP, onClick); + } + + private void stopGenerating(String prompt) { + getReloadResponseButton().setMode(GenerateButton.Mode.REFRESH, () -> call(prompt, true)); + } + + private GenerateButton getReloadResponseButton() { + return ((GenerateButton) reloadResponseButton); + } + + private void createUIComponents() { + textAreaScrollPane = new JBScrollPane() { + public JScrollBar createVerticalScrollBar() { + JScrollBar verticalScrollBar = new JScrollPane.ScrollBar(1); + verticalScrollBar.setPreferredSize(new Dimension(0, 0)); + return verticalScrollBar; + } + }; + textAreaScrollPane.setBorder(null); + textAreaScrollPane.setViewportBorder(null); + textAreaScrollPane.setBorder(BorderFactory.createCompoundBorder( + BorderFactory.createMatteBorder(1, 0, 0, 0, JBColor.border()), + BorderFactory.createEmptyBorder(0, 5, 0, 10))); + textArea = new TextArea(this::handleSubmit, textAreaScrollPane); + textAreaScrollPane.setViewportView(textArea); + markdownHtmlPanel = new MarkdownJCEFHtmlPanel(); + contentPanel = new JPanel(new BorderLayout()); + contentPanel.setBorder(JBUI.Borders.emptyTop(8)); + contentPanel.add(markdownHtmlPanel.getComponent(), BorderLayout.CENTER); + contentPanel.setPreferredSize(markdownHtmlPanel.getComponent().getPreferredSize()); + reloadResponseButton = new GenerateButton(); + } +} 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 cff23a92..089684dc 100644 --- a/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ChatToolWindowTabPanel.java +++ b/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ChatToolWindowTabPanel.java @@ -11,13 +11,13 @@ import com.intellij.openapi.project.Project; import com.intellij.openapi.roots.ui.componentsList.components.ScrollablePanel; import com.intellij.ui.JBColor; import com.intellij.ui.components.JBScrollPane; +import ee.carlrobert.codegpt.client.RequestHandler; 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.state.settings.SettingsConfigurable; import ee.carlrobert.codegpt.state.settings.SettingsState; -import ee.carlrobert.codegpt.client.RequestHandler; import ee.carlrobert.codegpt.toolwindow.components.GenerateButton; import ee.carlrobert.codegpt.toolwindow.components.LandingView; import ee.carlrobert.codegpt.toolwindow.components.ScrollPane; @@ -43,7 +43,7 @@ import javax.swing.SwingUtilities; import org.fife.ui.rsyntaxtextarea.SyntaxConstants; import org.jetbrains.annotations.NotNull; -public class ChatToolWindowTabPanel { +public class ChatToolWindowTabPanel implements ToolWindowTabPanel { private final List textAreas = new ArrayList<>(); private final Project project; @@ -60,25 +60,17 @@ public class ChatToolWindowTabPanel { this.project = project; } + @Override public JPanel getContent() { return chatGptToolWindowContent; } + @Override public Conversation getConversation() { return conversation; } - public void setConversation(Conversation conversation) { - this.conversation = conversation; - } - - public void displayUserMessage(String userMessage) { - addIconLabel(AllIcons.General.User, AccountDetailsState.getInstance().accountName); - scrollablePanel.add(createTextPane(userMessage)); - scrollablePanel.revalidate(); - scrollablePanel.repaint(); - } - + @Override public void displayLandingView() { if (!isLandingViewVisible) { SwingUtilities.invokeLater(() -> { @@ -99,6 +91,7 @@ public class ChatToolWindowTabPanel { } } + @Override public void displayConversation(Conversation conversation) { setConversation(conversation); clearWindow(); @@ -116,11 +109,14 @@ public class ChatToolWindowTabPanel { scrollablePanel.repaint(); } - public void sendMessage(String prompt, Project project) { - sendMessage(prompt, project, false); + @Override + public void startNewConversation(String prompt) { + conversation = ConversationsState.getInstance().startConversation(); + startConversation(prompt, false); } - public void sendMessage(String prompt, Project project, boolean isRetry) { + @Override + public void startConversation(String prompt, boolean isRetry) { if (!isRetry) { addIconLabel(Icons.DefaultImageIcon, "ChatGPT"); } @@ -142,6 +138,17 @@ public class ChatToolWindowTabPanel { } } + public void setConversation(Conversation conversation) { + this.conversation = conversation; + } + + public void displayUserMessage(String userMessage) { + addIconLabel(AllIcons.General.User, AccountDetailsState.getInstance().accountName); + scrollablePanel.add(createTextPane(userMessage)); + scrollablePanel.revalidate(); + scrollablePanel.repaint(); + } + public void call(SyntaxTextArea textArea, String prompt, boolean isRetry) { if (conversation == null) { conversation = ConversationsState.getInstance().startConversation(); @@ -149,10 +156,9 @@ public class ChatToolWindowTabPanel { var conversationMessage = new Message(prompt); var requestService = new RequestHandler(conversation) { - public void handleMessage(String message) { + public void handleMessage(String message, String fullMessage) { try { textArea.append(message); - conversationMessage.setResponse(textArea.getText()); scrollToBottom(); } catch (Exception e) { textArea.append("Something went wrong. Please try again later."); @@ -207,7 +213,7 @@ public class ChatToolWindowTabPanel { public void stopGenerating(String prompt, SyntaxTextArea textArea) { generateButton.setMode(GenerateButton.Mode.REFRESH, () -> { - sendMessage(prompt, project, true); + startConversation(prompt, true); scrollToBottom(); }); textArea.displayCopyButton(); @@ -228,7 +234,7 @@ public class ChatToolWindowTabPanel { setConversation(ConversationsState.getInstance().startConversation()); } displayUserMessage(searchText); - sendMessage(searchText, project); + startNewConversation(searchText); textArea.setText(""); scrollToBottom(); scrollablePanel.revalidate(); diff --git a/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/HtmlPanelPopupMenu.java b/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/HtmlPanelPopupMenu.java new file mode 100644 index 00000000..1bf1fe92 --- /dev/null +++ b/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/HtmlPanelPopupMenu.java @@ -0,0 +1,51 @@ +package ee.carlrobert.codegpt.toolwindow.chat; + +import com.intellij.icons.AllIcons; +import java.awt.Toolkit; +import java.awt.datatransfer.Clipboard; +import java.awt.datatransfer.StringSelection; +import java.awt.event.MouseAdapter; +import java.awt.event.MouseEvent; +import javax.swing.JMenuItem; +import javax.swing.JPopupMenu; + +class HtmlPanelPopupMenu extends JPopupMenu { + + private final JMenuItem copyMenuItem; + private final JMenuItem replaceMenuItem; + + HtmlPanelPopupMenu() { + super(); + copyMenuItem = new JMenuItem("Copy", AllIcons.Actions.Copy); + replaceMenuItem = new JMenuItem("Replace Editor Selection", AllIcons.Actions.Replace); + add(copyMenuItem); + add(replaceMenuItem); + } + + MouseAdapter getMouseAdapter() { + return new MouseAdapter() { + public void mousePressed(MouseEvent e) { + if (e.isPopupTrigger()) { + showPopupMenu(e.getX(), e.getY()); + } + } + + public void mouseReleased(MouseEvent e) { + if (e.isPopupTrigger()) { + showPopupMenu(e.getX(), e.getY()); + } + } + + private void showPopupMenu(int x, int y) { + copyMenuItem.addActionListener(e -> { + Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard(); + // String selection = (String) browser.executeJavaScriptAndReturnValue("window.getSelection().toString();").getValue(); + StringSelection data = new StringSelection("selection"); + clipboard.setContents(data, null); + }); + replaceMenuItem.addActionListener(e -> {}); + show(getComponent(), x, y); + } + }; + } +} diff --git a/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/MarkdownJCEFHtmlPanel.java b/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/MarkdownJCEFHtmlPanel.java new file mode 100644 index 00000000..6e21d4bb --- /dev/null +++ b/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/MarkdownJCEFHtmlPanel.java @@ -0,0 +1,187 @@ +package ee.carlrobert.codegpt.toolwindow.chat; + +import static java.lang.String.format; + +import com.intellij.openapi.editor.colors.EditorColorsManager; +import com.intellij.ui.jcef.JCEFHtmlPanel; +import com.intellij.util.ui.JBFont; +import com.intellij.util.ui.JBUI; +import com.intellij.util.ui.UIUtil; +import ee.carlrobert.codegpt.state.AccountDetailsState; +import ee.carlrobert.codegpt.state.conversations.Conversation; +import java.awt.Color; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; +import org.cef.browser.CefBrowser; +import org.cef.browser.CefFrame; +import org.cef.handler.CefLoadHandlerAdapter; + +public class MarkdownJCEFHtmlPanel extends JCEFHtmlPanel { + + private final CompletableFuture isLoaded = new CompletableFuture<>(); + private final BrowserContentManager browserContentManager; + private UUID previousResponseId; + + public MarkdownJCEFHtmlPanel() { + super(null); + this.browserContentManager = new BrowserContentManager(getCefBrowser()); + setPageBackgroundColor(getRGB(UIUtil.getPanelBackground())); + setHtml(getIndexContent()); + getJBCefClient().addLoadHandler(new CefLoadHandlerAdapter() { + public void onLoadEnd(CefBrowser browser, CefFrame frame, int httpStatusCode) { + if (httpStatusCode == 200) { + isLoaded.complete(null); + } + } + }, getCefBrowser()); + + /*var popupMenu = new HtmlPanelPopupMenu(); + getComponent().addMouseListener(popupMenu.getMouseAdapter()); + getComponent().setComponentPopupMenu(popupMenu);*/ + } + + public void displayUserMessage(String prompt) { + var name = AccountDetailsState.getInstance().accountName; + browserContentManager.displayUserMessage(name == null || name.isEmpty() ? "User" : name, prompt); + } + + public UUID prepareResponse(boolean animate, boolean isRetry) { + if (isRetry) { + browserContentManager.clearResponse(previousResponseId); + browserContentManager.animateSvg(previousResponseId); + } else { + previousResponseId = UUID.randomUUID(); + browserContentManager.displayResponse(previousResponseId, animate); + } + return previousResponseId; + } + + public void displayConversation(Conversation conversation) { + runWhenLoaded(() -> conversation.getMessages().forEach(message -> { + displayUserMessage(message.getPrompt()); + replaceHtml(prepareResponse(false, false), message.getResponse()); + })); + } + + public void displayErrorMessage() { + displayErrorMessage("Something went wrong. Please try again later."); + } + + public void displayErrorMessage(String errorMessage) { + browserContentManager.displayErrorMessage(previousResponseId, errorMessage); + } + + public void displayMissingCredential() { + browserContentManager.displayMissingCredential(prepareResponse(false, false)); + } + + public void displayLandingView() { + runWhenLoaded(browserContentManager::displayLandingView); + } + + public void replaceHtml(UUID responseId, String message) { + browserContentManager.replaceResponseContent(responseId, message); + } + + public void stopTyping() { + browserContentManager.stopTyping(); + } + + public void runWhenLoaded(Runnable runnable) { + isLoaded.thenRun(runnable); + } + + private String getIndexContent() { + var panelBg = UIUtil.getPanelBackground(); + var theme = UIUtil.isUnderDarcula() ? "darcula" : "vs"; + var bgColor = getRGB(UIUtil.isUnderDarcula() ? toDarker(panelBg) : panelBg.brighter()); + var separatorColor = JBUI.CurrentTheme.CustomFrameDecorations.separatorForeground(); + return "" + + "" + + format( + "", + theme) + + "" + + "" + + "" + + "" + + "" + + ""; + } + + private static Color toDarker(Color color) { + var factor = 0.9; + return new Color( + Math.max((int) (color.getRed() * factor), 0), + Math.max((int) (color.getGreen() * factor), 0), + Math.max((int) (color.getBlue() * factor), 0), + color.getAlpha()); + } + + private static String getForegroundRGB() { + return getRGB(EditorColorsManager.getInstance().getSchemeForCurrentUITheme().getDefaultForeground()); + } + + private static String getRGB(Color color) { + return format("rgb(%d, %d, %d)", color.getRed(), color.getGreen(), color.getBlue()); + } +} diff --git a/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ToolWindowTabPanel.java b/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ToolWindowTabPanel.java new file mode 100644 index 00000000..a8930b15 --- /dev/null +++ b/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ToolWindowTabPanel.java @@ -0,0 +1,20 @@ +package ee.carlrobert.codegpt.toolwindow.chat; + +import ee.carlrobert.codegpt.state.conversations.Conversation; +import javax.swing.JPanel; + +public interface ToolWindowTabPanel { + + JPanel getContent(); + + Conversation getConversation(); + + void displayLandingView(); + + void displayConversation(Conversation conversation); + + void startNewConversation(String prompt); + + void startConversation(String prompt, boolean isRetry); + +} diff --git a/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ToolWindowTabPanelFactory.java b/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ToolWindowTabPanelFactory.java new file mode 100644 index 00000000..a0c73c44 --- /dev/null +++ b/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ToolWindowTabPanelFactory.java @@ -0,0 +1,15 @@ +package ee.carlrobert.codegpt.toolwindow.chat; + +import com.intellij.openapi.project.Project; +import com.intellij.ui.jcef.JBCefApp; +import org.jetbrains.annotations.NotNull; + +public class ToolWindowTabPanelFactory { + + public static ToolWindowTabPanel getTabPanel(@NotNull Project project) { + if (JBCefApp.isSupported()) { + return new ChatToolWindowTabHtmlPanel(project); + } + return new ChatToolWindowTabPanel(project); + } +} diff --git a/src/main/java/ee/carlrobert/codegpt/toolwindow/components/ScrollPane.java b/src/main/java/ee/carlrobert/codegpt/toolwindow/components/ScrollPane.java index 1076b137..5b86097d 100644 --- a/src/main/java/ee/carlrobert/codegpt/toolwindow/components/ScrollPane.java +++ b/src/main/java/ee/carlrobert/codegpt/toolwindow/components/ScrollPane.java @@ -5,12 +5,13 @@ import com.intellij.ui.components.JBScrollPane; import java.awt.Adjustable; import java.awt.event.AdjustmentEvent; import java.awt.event.AdjustmentListener; +import javax.swing.JComponent; import javax.swing.JScrollBar; import javax.swing.ScrollPaneConstants; public class ScrollPane extends JBScrollPane { - public ScrollPane(ScrollablePanel scrollablePanel) { + public ScrollPane(JComponent scrollablePanel) { super(scrollablePanel); setHorizontalScrollBarPolicy(ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER); setBorder(null); diff --git a/src/main/java/ee/carlrobert/codegpt/toolwindow/components/TextArea.java b/src/main/java/ee/carlrobert/codegpt/toolwindow/components/TextArea.java index b144e90e..8a8de7c7 100644 --- a/src/main/java/ee/carlrobert/codegpt/toolwindow/components/TextArea.java +++ b/src/main/java/ee/carlrobert/codegpt/toolwindow/components/TextArea.java @@ -8,18 +8,15 @@ import com.intellij.ui.components.JBTextArea; import com.intellij.util.ui.JBUI; import icons.Icons; import java.awt.event.ActionListener; -import java.awt.event.FocusEvent; -import java.awt.event.FocusListener; import javax.swing.JButton; import javax.swing.JScrollPane; public class TextArea extends JBTextArea { public TextArea(Runnable onSubmit, JScrollPane textAreaScrollPane) { - super("Ask me anything..."); - setForeground(JBColor.GRAY); + super(); + getEmptyText().setText("Ask me anything..."); setMargin(JBUI.insets(5)); - addFocusListener(getFocusListener()); addSubmitButton(onSubmit, textAreaScrollPane); addShiftEnterInputMap(this, onSubmit); } @@ -36,22 +33,4 @@ public class TextArea extends JBTextArea { button.addActionListener(submitButtonListener); return button; } - - private FocusListener getFocusListener() { - return new FocusListener() { - public void focusGained(FocusEvent e) { - if (getText().equals("Ask me anything...")) { - setText(""); - setForeground(JBColor.BLACK); - } - } - - public void focusLost(FocusEvent e) { - if (getText().isEmpty()) { - setForeground(JBColor.GRAY); - setText("Ask me anything..."); - } - } - }; - } } diff --git a/src/main/java/ee/carlrobert/codegpt/toolwindow/conversations/ConversationsToolWindow.java b/src/main/java/ee/carlrobert/codegpt/toolwindow/conversations/ConversationsToolWindow.java index 309e1d22..834c308c 100644 --- a/src/main/java/ee/carlrobert/codegpt/toolwindow/conversations/ConversationsToolWindow.java +++ b/src/main/java/ee/carlrobert/codegpt/toolwindow/conversations/ConversationsToolWindow.java @@ -13,7 +13,7 @@ import ee.carlrobert.codegpt.state.conversations.Conversation; import ee.carlrobert.codegpt.state.conversations.ConversationsState; import ee.carlrobert.codegpt.state.settings.SettingsState; import ee.carlrobert.codegpt.toolwindow.chat.ChatContentManagerService; -import ee.carlrobert.codegpt.toolwindow.chat.ChatToolWindowTabPanel; +import ee.carlrobert.codegpt.toolwindow.chat.ToolWindowTabPanelFactory; import ee.carlrobert.codegpt.toolwindow.conversations.actions.ClearAllConversationsAction; import ee.carlrobert.codegpt.toolwindow.conversations.actions.DeleteConversationAction; import ee.carlrobert.codegpt.toolwindow.conversations.actions.MoveDownAction; @@ -83,7 +83,7 @@ public class ConversationsToolWindow { .ifPresentOrElse( title -> tabbedPane.setSelectedIndex(tabbedPane.indexOfTab(title)), () -> { - var panel = new ChatToolWindowTabPanel(project); + var panel = ToolWindowTabPanelFactory.getTabPanel(project); panel.displayConversation(conversation); tabbedPane.addNewTab(panel); })); diff --git a/src/main/java/ee/carlrobert/codegpt/util/FileUtils.java b/src/main/java/ee/carlrobert/codegpt/util/FileUtils.java new file mode 100644 index 00000000..e9171f74 --- /dev/null +++ b/src/main/java/ee/carlrobert/codegpt/util/FileUtils.java @@ -0,0 +1,17 @@ +package ee.carlrobert.codegpt.util; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class FileUtils { + + public static String getFileExtension(String filename) { + Pattern pattern = Pattern.compile("[^.]+$"); + Matcher matcher = pattern.matcher(filename); + + if (matcher.find()) { + return matcher.group(); + } + return ""; + } +}