From d672d2847485d651c1f1e4d2ba6a8e605f8be85a Mon Sep 17 00:00:00 2001 From: Carl-Robert Linnupuu Date: Thu, 22 Aug 2024 18:40:50 +0300 Subject: [PATCH] feat: display web docs progress --- build.gradle.kts | 1 + .../java/ee/carlrobert/codegpt/Icons.java | 2 + .../chat/ChatToolWindowTabPanel.java | 9 +- ...WindowCompletionResponseEventListener.java | 3 +- .../chat/ui/ChatMessageResponseBody.java | 99 +++++++++++++++++-- .../toolwindow/chat/ui/UserMessagePanel.java | 10 +- .../carlrobert/codegpt/events/CodeGPTEvent.kt | 85 ++++++++++++++-- .../codegpt/toolwindow/ui/WebpageList.kt | 7 +- src/main/resources/icons/greenCheckmark.svg | 3 + .../resources/icons/greenCheckmark_dark.svg | 3 + .../resources/messages/codegpt.properties | 5 +- 11 files changed, 195 insertions(+), 32 deletions(-) create mode 100644 src/main/resources/icons/greenCheckmark.svg create mode 100644 src/main/resources/icons/greenCheckmark_dark.svg diff --git a/build.gradle.kts b/build.gradle.kts index 28f3c78b..7a4c6634 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -57,6 +57,7 @@ dependencies { implementation(platform(libs.jackson.bom)) implementation("com.fasterxml.jackson.datatype:jackson-datatype-jdk8") implementation("com.fasterxml.jackson.datatype:jackson-datatype-jsr310") + implementation("com.fasterxml.jackson.module:jackson-module-kotlin") implementation(libs.flexmark.all) { // vulnerable transitive dependency exclude(group = "org.jsoup", module = "jsoup") diff --git a/src/main/java/ee/carlrobert/codegpt/Icons.java b/src/main/java/ee/carlrobert/codegpt/Icons.java index 7d5eab46..5cd2dc58 100644 --- a/src/main/java/ee/carlrobert/codegpt/Icons.java +++ b/src/main/java/ee/carlrobert/codegpt/Icons.java @@ -25,4 +25,6 @@ public final class Icons { public static final Icon Ollama = IconLoader.getIcon("/icons/ollama.svg", Icons.class); public static final Icon User = IconLoader.getIcon("/icons/user.svg", Icons.class); public static final Icon Upload = IconLoader.getIcon("/icons/upload.svg", Icons.class); + public static final Icon GreenCheckmark = + IconLoader.getIcon("/icons/greenCheckmark.svg", Icons.class); } 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 9eecaae4..298d2179 100644 --- a/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ChatToolWindowTabPanel.java +++ b/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ChatToolWindowTabPanel.java @@ -31,6 +31,7 @@ import ee.carlrobert.codegpt.toolwindow.ui.ChatToolWindowLandingPanel; import ee.carlrobert.codegpt.ui.OverlayUtil; import ee.carlrobert.codegpt.ui.textarea.AppliedActionInlay; import ee.carlrobert.codegpt.ui.textarea.UserInputPanel; +import ee.carlrobert.codegpt.ui.textarea.suggestion.item.CreateDocumentationActionItem; import ee.carlrobert.codegpt.ui.textarea.suggestion.item.DocumentationActionItem; import ee.carlrobert.codegpt.ui.textarea.suggestion.item.PersonaActionItem; import ee.carlrobert.codegpt.ui.textarea.suggestion.item.WebSearchActionItem; @@ -174,7 +175,8 @@ public class ChatToolWindowTabPanel implements Disposable { .withReloadAction(() -> reloadMessage(message, conversation, conversationType)) .withDeleteAction(() -> removeMessage(message.getId(), conversation)) .addContent( - new ChatMessageResponseBody(project, true, false, message.isWebSearchIncluded(), this)); + new ChatMessageResponseBody(project, true, false, message.isWebSearchIncluded(), + message.getDocumentationDetails() != null, this)); } private void reloadMessage( @@ -263,7 +265,8 @@ public class ChatToolWindowTabPanel implements Disposable { var addedDocumentation = CodeGPTKeys.ADDED_DOCUMENTATION.get(project); var appliedInlayExists = appliedInlayActions.stream() - .anyMatch(it -> it.getSuggestion() instanceof DocumentationActionItem); + .anyMatch(it -> it.getSuggestion() instanceof DocumentationActionItem + || it.getSuggestion() instanceof CreateDocumentationActionItem); if (addedDocumentation != null && appliedInlayExists) { message.setDocumentationDetails(addedDocumentation); CodeGPTKeys.ADDED_DOCUMENTATION.set(project, null); @@ -271,7 +274,7 @@ public class ChatToolWindowTabPanel implements Disposable { var addedPersona = CodeGPTKeys.ADDED_PERSONA.get(project); var personaInlayExists = appliedInlayActions.stream() - .anyMatch(it -> it.getSuggestion() instanceof PersonaActionItem); + .anyMatch(it -> it.getSuggestion() instanceof PersonaActionItem); if (addedPersona != null && personaInlayExists) { message.setPersonaDetails(addedPersona); CodeGPTKeys.ADDED_PERSONA.set(project, null); diff --git a/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ToolWindowCompletionResponseEventListener.java b/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ToolWindowCompletionResponseEventListener.java index 0d6edc6a..7d7615e0 100644 --- a/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ToolWindowCompletionResponseEventListener.java +++ b/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ToolWindowCompletionResponseEventListener.java @@ -121,8 +121,7 @@ abstract class ToolWindowCompletionResponseEventListener implements @Override public void handleCodeGPTEvent(CodeGPTEvent event) { - ApplicationManager.getApplication().invokeLater(() -> - responseContainer.displayWebSearchItem(event.getEvent().getDetails())); + responseContainer.handleCodeGPTEvent(event); } private void stopStreaming(ChatMessageResponseBody responseContainer) { diff --git a/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ui/ChatMessageResponseBody.java b/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ui/ChatMessageResponseBody.java index ef3a06ef..d1fcdf09 100644 --- a/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ui/ChatMessageResponseBody.java +++ b/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ui/ChatMessageResponseBody.java @@ -5,6 +5,7 @@ import static ee.carlrobert.codegpt.util.MarkdownUtil.convertMdToHtml; import static java.lang.String.format; import static javax.swing.event.HyperlinkEvent.EventType.ACTIVATED; +import com.intellij.icons.AllIcons.General; import com.intellij.openapi.Disposable; import com.intellij.openapi.application.ApplicationManager; import com.intellij.openapi.fileEditor.FileEditorManager; @@ -14,12 +15,19 @@ import com.intellij.openapi.util.io.FileUtil; import com.intellij.openapi.vfs.LocalFileSystem; import com.intellij.openapi.vfs.VirtualFile; import com.intellij.ui.components.JBLabel; +import com.intellij.util.ui.AsyncProcessIcon; +import com.intellij.util.ui.JBFont; import com.intellij.util.ui.JBUI; import com.vladsch.flexmark.ast.FencedCodeBlock; import com.vladsch.flexmark.parser.Parser; import ee.carlrobert.codegpt.CodeGPTBundle; +import ee.carlrobert.codegpt.Icons; import ee.carlrobert.codegpt.actions.ActionType; -import ee.carlrobert.codegpt.events.Details; +import ee.carlrobert.codegpt.events.AnalysisCompletedEventDetails; +import ee.carlrobert.codegpt.events.AnalysisFailedEventDetails; +import ee.carlrobert.codegpt.events.CodeGPTEvent; +import ee.carlrobert.codegpt.events.EventDetails; +import ee.carlrobert.codegpt.events.WebSearchEventDetails; import ee.carlrobert.codegpt.settings.GeneralSettingsConfigurable; import ee.carlrobert.codegpt.telemetry.TelemetryAction; import ee.carlrobert.codegpt.toolwindow.chat.StreamParser; @@ -29,11 +37,16 @@ import ee.carlrobert.codegpt.ui.UIUtil; import ee.carlrobert.codegpt.util.EditorUtil; import ee.carlrobert.codegpt.util.MarkdownUtil; import java.awt.BorderLayout; +import java.awt.Component; +import java.awt.FlowLayout; import java.util.Objects; +import javax.swing.Box; import javax.swing.BoxLayout; import javax.swing.DefaultListModel; +import javax.swing.Icon; import javax.swing.JPanel; import javax.swing.JTextPane; +import javax.swing.SwingConstants; public class ChatMessageResponseBody extends JPanel { @@ -41,8 +54,10 @@ public class ChatMessageResponseBody extends JPanel { private final Disposable parentDisposable; private final StreamParser streamParser; private final boolean readOnly; - private final DefaultListModel
webpageListModel = new DefaultListModel<>(); + private final DefaultListModel webpageListModel = new DefaultListModel<>(); private final WebpageList webpageList = new WebpageList(webpageListModel); + private final JPanel webDocProgressContainer = new JPanel(); + private final AsyncProcessIcon spinner = new AsyncProcessIcon("sign_in_spinner"); private ResponseEditorPanel currentlyProcessedEditorPanel; private JTextPane currentlyProcessedTextPane; private JPanel webpageListPanel; @@ -56,7 +71,7 @@ public class ChatMessageResponseBody extends JPanel { Project project, boolean withGhostText, Disposable parentDisposable) { - this(project, withGhostText, false, false, parentDisposable); + this(project, withGhostText, false, false, false, parentDisposable); } public ChatMessageResponseBody( @@ -64,13 +79,14 @@ public class ChatMessageResponseBody extends JPanel { boolean withGhostText, boolean readOnly, boolean webSearchIncluded, + boolean webDocIncluded, Disposable parentDisposable) { super(new BorderLayout()); this.project = project; this.parentDisposable = parentDisposable; this.streamParser = new StreamParser(); this.readOnly = readOnly; - setLayout(new BoxLayout(this, BoxLayout.PAGE_AXIS)); + setLayout(new BoxLayout(this, BoxLayout.Y_AXIS)); setOpaque(false); if (webSearchIncluded) { @@ -78,6 +94,12 @@ public class ChatMessageResponseBody extends JPanel { add(webpageListPanel); } + if (webDocIncluded) { + webDocProgressContainer.setLayout(new BoxLayout(webDocProgressContainer, BoxLayout.Y_AXIS)); + webDocProgressContainer.setBorder(JBUI.Borders.emptyBottom(8)); + add(webDocProgressContainer); + } + if (withGhostText) { prepareProcessingText(!readOnly); currentlyProcessedTextPane.setText( @@ -161,10 +183,30 @@ public class ChatMessageResponseBody extends JPanel { }); } - public void displayWebSearchItem(Details details) { - webpageListModel.addElement(details); - webpageList.revalidate(); - webpageList.repaint(); + public void handleCodeGPTEvent(CodeGPTEvent codegptEvent) { + ApplicationManager.getApplication() + .invokeLater(() -> { + var event = codegptEvent.getEvent(); + if (event.getDetails() instanceof WebSearchEventDetails webSearchEventDetails) { + displayWebSearchItem(webSearchEventDetails); + return; + } + + switch (event.getType()) { + case WEB_SEARCH_ITEM -> { + if (event.getDetails() != null + && event.getDetails() instanceof WebSearchEventDetails eventDetails) { + displayWebSearchItem(eventDetails); + } + } + + case ANALYZE_WEB_DOC_STARTED -> showWebDocsProgress(); + case ANALYZE_WEB_DOC_COMPLETED -> completeWebDocsProgress(event.getDetails()); + case ANALYZE_WEB_DOC_FAILED -> failWebDocsProgress(event.getDetails()); + default -> { + } + } + }); } public void hideCaret() { @@ -236,6 +278,45 @@ public class ChatMessageResponseBody extends JPanel { add(currentlyProcessedEditorPanel); } + private void displayWebSearchItem(WebSearchEventDetails details) { + webpageListModel.addElement(details); + webpageList.revalidate(); + webpageList.repaint(); + } + + private void showWebDocsProgress() { + var wrapper = new JPanel(new FlowLayout(FlowLayout.LEADING, 0, 0)); + wrapper.add(spinner); + wrapper.add(Box.createHorizontalStrut(4)); + wrapper.add(new JBLabel( + CodeGPTBundle.get("chatMessageResponseBody.webDocs.startProgress.label")).withFont( + JBFont.small())); + updateWebDocsProgress(wrapper); + } + + private void completeWebDocsProgress(EventDetails eventDetails) { + if (eventDetails instanceof AnalysisCompletedEventDetails defaultEventDetails) { + updateWebDocsProgressLabel(defaultEventDetails.getDescription(), Icons.GreenCheckmark); + } + } + + private void failWebDocsProgress(EventDetails eventDetails) { + if (eventDetails instanceof AnalysisFailedEventDetails failedEventDetails) { + updateWebDocsProgressLabel(failedEventDetails.getError(), General.Error); + } + } + + private void updateWebDocsProgressLabel(String text, Icon icon) { + updateWebDocsProgress(new JBLabel(text, icon, SwingConstants.LEADING).withFont(JBFont.small())); + } + + private void updateWebDocsProgress(Component content) { + webDocProgressContainer.removeAll(); + webDocProgressContainer.add(JBUI.Panels.simplePanel(content)); + webDocProgressContainer.revalidate(); + webDocProgressContainer.repaint(); + } + private JTextPane createTextPane(String text, boolean caretVisible) { var textPane = UIUtil.createTextPane(text, false, event -> { if (FileUtil.exists(event.getDescription()) && ACTIVATED.equals(event.getEventType())) { @@ -258,7 +339,7 @@ public class ChatMessageResponseBody extends JPanel { var title = new JPanel(new BorderLayout()); title.setOpaque(false); title.setBorder(JBUI.Borders.empty(8, 0)); - title.add(new JBLabel(CodeGPTBundle.get("chatMessageResponseBody.webPagesTitle")) + title.add(new JBLabel(CodeGPTBundle.get("chatMessageResponseBody.webPages.title")) .withFont(JBUI.Fonts.miniFont()), BorderLayout.LINE_START); var listPanel = new JPanel(new BorderLayout()); listPanel.add(webpageList, BorderLayout.LINE_START); diff --git a/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ui/UserMessagePanel.java b/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ui/UserMessagePanel.java index 31164286..f9588e37 100644 --- a/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ui/UserMessagePanel.java +++ b/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ui/UserMessagePanel.java @@ -12,7 +12,7 @@ import ee.carlrobert.codegpt.CodeGPTBundle; import ee.carlrobert.codegpt.CodeGPTKeys; import ee.carlrobert.codegpt.Icons; import ee.carlrobert.codegpt.conversations.message.Message; -import ee.carlrobert.codegpt.events.Details; +import ee.carlrobert.codegpt.events.WebSearchEventDetails; import ee.carlrobert.codegpt.settings.GeneralSettings; import ee.carlrobert.codegpt.toolwindow.ui.WebpageList; import java.awt.BorderLayout; @@ -66,9 +66,10 @@ public class UserMessagePanel extends JPanel { var panel = new JPanel(new BorderLayout()); panel.setOpaque(false); if (addedDocumentation != null) { - var listModel = new DefaultListModel
(); - listModel.addElement(new Details(UUID.randomUUID().toString(), addedDocumentation.getName(), - addedDocumentation.getUrl(), addedDocumentation.getUrl())); + var listModel = new DefaultListModel(); + listModel.addElement( + new WebSearchEventDetails(UUID.randomUUID(), addedDocumentation.getName(), + addedDocumentation.getUrl(), addedDocumentation.getUrl())); panel.add(createWebpageListPanel(new WebpageList(listModel)), BorderLayout.NORTH); } @@ -99,6 +100,7 @@ public class UserMessagePanel extends JPanel { false, true, false, + false, parentDisposable) .withResponse(prompt); } diff --git a/src/main/kotlin/ee/carlrobert/codegpt/events/CodeGPTEvent.kt b/src/main/kotlin/ee/carlrobert/codegpt/events/CodeGPTEvent.kt index 0a3a6820..6fdc97cd 100644 --- a/src/main/kotlin/ee/carlrobert/codegpt/events/CodeGPTEvent.kt +++ b/src/main/kotlin/ee/carlrobert/codegpt/events/CodeGPTEvent.kt @@ -2,19 +2,86 @@ package ee.carlrobert.codegpt.events import com.fasterxml.jackson.annotation.JsonCreator import com.fasterxml.jackson.annotation.JsonProperty +import com.fasterxml.jackson.core.JsonParser +import com.fasterxml.jackson.databind.DeserializationContext +import com.fasterxml.jackson.databind.JsonNode +import com.fasterxml.jackson.databind.ObjectMapper +import com.fasterxml.jackson.databind.annotation.JsonDeserialize +import com.fasterxml.jackson.databind.deser.std.StdDeserializer +import com.fasterxml.jackson.module.kotlin.registerKotlinModule +import ee.carlrobert.codegpt.events.Event.EventType +import java.util.* data class CodeGPTEvent @JsonCreator constructor( @JsonProperty("event") val event: Event ) +@JsonDeserialize(using = EventDeserializer::class) data class Event @JsonCreator constructor( - @JsonProperty("details") val details: Details, - @JsonProperty("type") val type: String -) + val details: EventDetails?, + val type: EventType +) { + enum class EventType { + ANALYZE_WEB_DOC_STARTED, + ANALYZE_WEB_DOC_COMPLETED, + ANALYZE_WEB_DOC_FAILED, + WEB_SEARCH_ITEM + } +} -data class Details @JsonCreator constructor( - @JsonProperty("id") val id: String, - @JsonProperty("name") val name: String, - @JsonProperty("url") val url: String, - @JsonProperty("displayUrl") val displayUrl: String -) +interface EventDetails + +data class WebSearchEventDetails( + val id: UUID, + val name: String, + val url: String, + val displayUrl: String +) : EventDetails + +data class AnalysisCompletedEventDetails( + val id: UUID, + val name: String, + val description: String, + val rerankedResults: List +) : EventDetails + +data class AnalysisFailedEventDetails( + val id: UUID, + val name: String, + val description: String, + val error: String +) : EventDetails + +data class DefaultEventDetails( + val id: UUID, + val name: String, + val description: String +) : EventDetails + +class EventDeserializer : StdDeserializer(Event::class.java) { + private val objectMapper = ObjectMapper().registerKotlinModule() + + override fun deserialize(parser: JsonParser, ctxt: DeserializationContext): Event { + val node: JsonNode = parser.codec.readTree(parser) + val type = EventType.valueOf(node.get("type").asText()) + val detailsNode = node.get("details") ?: return Event(null, type) + val eventDetails = when (type) { + EventType.WEB_SEARCH_ITEM -> { + objectMapper.treeToValue(detailsNode, WebSearchEventDetails::class.java) + } + + EventType.ANALYZE_WEB_DOC_COMPLETED -> { + objectMapper.treeToValue(detailsNode, AnalysisCompletedEventDetails::class.java) + } + + EventType.ANALYZE_WEB_DOC_FAILED -> { + objectMapper.treeToValue(detailsNode, AnalysisFailedEventDetails::class.java) + } + + else -> { + objectMapper.treeToValue(detailsNode, DefaultEventDetails::class.java) + } + } + return Event(eventDetails, type) + } +} diff --git a/src/main/kotlin/ee/carlrobert/codegpt/toolwindow/ui/WebpageList.kt b/src/main/kotlin/ee/carlrobert/codegpt/toolwindow/ui/WebpageList.kt index 05c69f7e..1b60744f 100644 --- a/src/main/kotlin/ee/carlrobert/codegpt/toolwindow/ui/WebpageList.kt +++ b/src/main/kotlin/ee/carlrobert/codegpt/toolwindow/ui/WebpageList.kt @@ -16,14 +16,15 @@ import com.intellij.ui.dsl.builder.panel import com.intellij.ui.jcef.JBCefApp import com.intellij.util.ui.JBUI import ee.carlrobert.codegpt.CodeGPTBundle -import ee.carlrobert.codegpt.events.Details +import ee.carlrobert.codegpt.events.WebSearchEventDetails import org.cef.browser.CefRendering import java.awt.* import java.awt.event.MouseAdapter import java.awt.event.MouseEvent import javax.swing.* -class WebpageList(model: DefaultListModel
) : JBList
(model) { +class WebpageList(model: DefaultListModel) : + JBList(model) { init { setModel(model) @@ -135,7 +136,7 @@ class WebpageListCellRenderer : DefaultListCellRenderer() { super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus).apply { setOpaque(false) }.let { component -> - if (component is JLabel && value is Details) { + if (component is JLabel && value is WebSearchEventDetails) { component.apply { icon = AllIcons.General.Web iconTextGap = 4 diff --git a/src/main/resources/icons/greenCheckmark.svg b/src/main/resources/icons/greenCheckmark.svg new file mode 100644 index 00000000..f9c7c89f --- /dev/null +++ b/src/main/resources/icons/greenCheckmark.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/main/resources/icons/greenCheckmark_dark.svg b/src/main/resources/icons/greenCheckmark_dark.svg new file mode 100644 index 00000000..ce333a7c --- /dev/null +++ b/src/main/resources/icons/greenCheckmark_dark.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/main/resources/messages/codegpt.properties b/src/main/resources/messages/codegpt.properties index 93675d0c..e1b7e81b 100644 --- a/src/main/resources/messages/codegpt.properties +++ b/src/main/resources/messages/codegpt.properties @@ -248,7 +248,8 @@ smartTextPane.submitButton.title=Send Message smartTextPane.submitButton.description=Send message smartTextPane.stopButton.title=Stop smartTextPane.stopButton.description=Stop completion -chatMessageResponseBody.webPagesTitle=WEB PAGES +chatMessageResponseBody.webPages.title=WEB PAGES +chatMessageResponseBody.webDocs.startProgress.label=Analyzing web content... addDocumentation.popup.title=Add Documentation addDocumentation.popup.form.name.label=Name: addDocumentation.popup.form.url.label=URL: @@ -262,4 +263,4 @@ suggestionGroupItem.docs.displayName=Docs suggestionActionItem.webSearch.displayName=Web suggestionActionItem.viewDocumentations.displayName=View all docs suggestionActionItem.createPersona.displayName=Create new persona -suggestionActionItem.createDocumentation.displayName=Create new documentation \ No newline at end of file +suggestionActionItem.createDocumentation.displayName=Create new documentation