feat: display web docs progress

This commit is contained in:
Carl-Robert Linnupuu 2024-08-22 18:40:50 +03:00
parent ce0b90f232
commit 03feba0a4f
11 changed files with 195 additions and 32 deletions

View file

@ -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")

View file

@ -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);
}

View file

@ -32,6 +32,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;
@ -175,7 +176,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(
@ -265,7 +267,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);
@ -273,7 +276,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);

View file

@ -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) {

View file

@ -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<Details> webpageListModel = new DefaultListModel<>();
private final DefaultListModel<WebSearchEventDetails> 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);

View file

@ -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<Details>();
listModel.addElement(new Details(UUID.randomUUID().toString(), addedDocumentation.getName(),
addedDocumentation.getUrl(), addedDocumentation.getUrl()));
var listModel = new DefaultListModel<WebSearchEventDetails>();
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);
}

View file

@ -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<String>
) : 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>(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)
}
}

View file

@ -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<Details>) : JBList<Details>(model) {
class WebpageList(model: DefaultListModel<WebSearchEventDetails>) :
JBList<WebSearchEventDetails>(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

View file

@ -0,0 +1,3 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M2.5 8.25L6 11.75L13.5 4.25" stroke="#369650" stroke-width="1.5" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 202 B

View file

@ -0,0 +1,3 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M2.5 8.25L6 11.75L13.5 4.25" stroke="#57965C" stroke-width="1.5" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 202 B

View file

@ -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
suggestionActionItem.createDocumentation.displayName=Create new documentation