From 2312a9dcb2f09a24112906102d76535bcf4e52f3 Mon Sep 17 00:00:00 2001 From: Carl-Robert Linnupuu Date: Fri, 18 Oct 2024 01:26:57 +0300 Subject: [PATCH] refactor: improve chat response rendering performance --- .../ChatCompletionEventListener.java | 5 + .../completions/CompletionRequestService.java | 7 +- .../CompletionResponseEventListener.java | 3 + .../chat/ChatToolWindowTabPanel.java | 5 +- ...WindowCompletionResponseEventListener.java | 33 +++++- .../chat/ui/ChatMessageResponseBody.java | 109 +++--------------- .../toolwindow/chat/ui/UserMessagePanel.java | 10 +- .../ui/ResponseBodyProgressPanel.kt | 71 ++++++++++++ 8 files changed, 132 insertions(+), 111 deletions(-) create mode 100644 src/main/kotlin/ee/carlrobert/codegpt/toolwindow/ui/ResponseBodyProgressPanel.kt diff --git a/src/main/java/ee/carlrobert/codegpt/completions/ChatCompletionEventListener.java b/src/main/java/ee/carlrobert/codegpt/completions/ChatCompletionEventListener.java index f515b677..4f4897da 100644 --- a/src/main/java/ee/carlrobert/codegpt/completions/ChatCompletionEventListener.java +++ b/src/main/java/ee/carlrobert/codegpt/completions/ChatCompletionEventListener.java @@ -21,6 +21,11 @@ public class ChatCompletionEventListener implements CompletionEventListener true; + case ANTHROPIC -> CredentialsStore.INSTANCE.isCredentialSet(CredentialKey.ANTHROPIC_API_KEY); case GOOGLE -> CredentialsStore.INSTANCE.isCredentialSet(CredentialKey.GOOGLE_API_KEY); + case CODEGPT, CUSTOM_OPENAI, LLAMA_CPP, OLLAMA -> true; }; } diff --git a/src/main/java/ee/carlrobert/codegpt/completions/CompletionResponseEventListener.java b/src/main/java/ee/carlrobert/codegpt/completions/CompletionResponseEventListener.java index 6144f868..f11f82ac 100644 --- a/src/main/java/ee/carlrobert/codegpt/completions/CompletionResponseEventListener.java +++ b/src/main/java/ee/carlrobert/codegpt/completions/CompletionResponseEventListener.java @@ -24,4 +24,7 @@ public interface CompletionResponseEventListener { default void handleCodeGPTEvent(CodeGPTEvent event) { } + + default void handleRequestOpen() { + } } 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 0abe3d5d..d219609b 100644 --- a/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ChatToolWindowTabPanel.java +++ b/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ChatToolWindowTabPanel.java @@ -235,8 +235,7 @@ public class ChatToolWindowTabPanel implements Disposable { true, false, message.isWebSearchIncluded(), - message.getDocumentationDetails() != null, - fileContextIncluded, + fileContextIncluded || message.getDocumentationDetails() != null, this)); } @@ -285,7 +284,7 @@ public class ChatToolWindowTabPanel implements Disposable { private void call(ChatCompletionParameters callParameters, ResponsePanel responsePanel) { var responseContainer = (ChatMessageResponseBody) responsePanel.getContent(); - if (!CompletionRequestService.getInstance().isAllowed()) { + if (!CompletionRequestService.isRequestAllowed()) { responseContainer.displayMissingCredential(); return; } 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 642c465a..d03a1c17 100644 --- a/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ToolWindowCompletionResponseEventListener.java +++ b/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ToolWindowCompletionResponseEventListener.java @@ -18,12 +18,15 @@ import ee.carlrobert.codegpt.toolwindow.chat.ui.textarea.TotalTokensPanel; import ee.carlrobert.codegpt.ui.OverlayUtil; import ee.carlrobert.codegpt.ui.textarea.UserInputPanel; import ee.carlrobert.llm.client.openai.completion.ErrorDetails; +import java.util.concurrent.ConcurrentLinkedQueue; +import javax.swing.Timer; abstract class ToolWindowCompletionResponseEventListener implements CompletionResponseEventListener { private static final Logger LOG = Logger.getInstance( ToolWindowCompletionResponseEventListener.class); + private static final int UPDATE_INTERVAL_MS = 8; private final StringBuilder messageBuilder = new StringBuilder(); private final EncodingManager encodingManager; @@ -33,7 +36,8 @@ abstract class ToolWindowCompletionResponseEventListener implements private final TotalTokensPanel totalTokensPanel; private final UserInputPanel textArea; - private volatile boolean completed; + private final Timer updateTimer = new Timer(UPDATE_INTERVAL_MS, e -> processBufferedMessages()); + private final ConcurrentLinkedQueue messageBuffer = new ConcurrentLinkedQueue<>(); public ToolWindowCompletionResponseEventListener( ConversationService conversationService, @@ -50,13 +54,20 @@ abstract class ToolWindowCompletionResponseEventListener implements public abstract void handleTokensExceededPolicyAccepted(); + @Override + public void handleRequestOpen() { + updateTimer.start(); + } + @Override public void handleMessage(String partialMessage) { try { messageBuilder.append(partialMessage); var ongoingTokens = encodingManager.countTokens(messageBuilder.toString()); - responseContainer.updateMessage(partialMessage); - totalTokensPanel.update(totalTokensPanel.getTokenDetails().getTotal() + ongoingTokens); + messageBuffer.offer(partialMessage); + ApplicationManager.getApplication().invokeLater(() -> + totalTokensPanel.update(totalTokensPanel.getTokenDetails().getTotal() + ongoingTokens) + ); } catch (Exception e) { responseContainer.displayError("Something went wrong."); throw new RuntimeException("Error while updating the content", e); @@ -122,8 +133,22 @@ abstract class ToolWindowCompletionResponseEventListener implements responseContainer.handleCodeGPTEvent(event); } + private void processBufferedMessages() { + if (messageBuffer.isEmpty()) { + return; + } + + StringBuilder accumulatedMessage = new StringBuilder(); + String message; + while ((message = messageBuffer.poll()) != null) { + accumulatedMessage.append(message); + } + + responseContainer.updateMessage(accumulatedMessage.toString()); + } + private void stopStreaming(ChatMessageResponseBody responseContainer) { - completed = true; + updateTimer.stop(); textArea.setSubmitEnabled(true); responseContainer.hideCaret(); } 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 098356fc..57998ea1 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 @@ -16,8 +16,6 @@ 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; @@ -28,28 +26,23 @@ 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.ProcessContextEventDetails; 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; import ee.carlrobert.codegpt.toolwindow.chat.editor.ResponseEditorPanel; +import ee.carlrobert.codegpt.toolwindow.ui.ResponseBodyProgressPanel; import ee.carlrobert.codegpt.toolwindow.ui.WebpageList; 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.JComponent; +import javax.swing.JEditorPane; import javax.swing.JPanel; import javax.swing.JTextPane; -import javax.swing.SwingConstants; import org.jetbrains.annotations.Nullable; public class ChatMessageResponseBody extends JPanel { @@ -62,18 +55,15 @@ public class ChatMessageResponseBody extends JPanel { private final boolean readOnly; private final DefaultListModel webpageListModel = new DefaultListModel<>(); private final WebpageList webpageList = new WebpageList(webpageListModel); - private final JPanel webDocProgressContainer = new JPanel(); - private final JPanel progressContainer = new JPanel(); - private final AsyncProcessIcon webDocsSpinner = new AsyncProcessIcon("web_docs_spinner"); - private final AsyncProcessIcon processSpinner = new AsyncProcessIcon("process_spinner"); + private final ResponseBodyProgressPanel progressPanel = new ResponseBodyProgressPanel(); private final @Nullable String highlightedText; private ResponseEditorPanel currentlyProcessedEditorPanel; - private JTextPane currentlyProcessedTextPane; + private JEditorPane currentlyProcessedTextPane; private JPanel webpageListPanel; private boolean responseReceived; public ChatMessageResponseBody(Project project, Disposable parentDisposable) { - this(project, null, false, false, false, false, false, parentDisposable); + this(project, null, false, false, false, false, parentDisposable); } public ChatMessageResponseBody( @@ -82,8 +72,7 @@ public class ChatMessageResponseBody extends JPanel { boolean withGhostText, boolean readOnly, boolean webSearchIncluded, - boolean webDocIncluded, - boolean fileContextIncluded, + boolean withProgress, Disposable parentDisposable) { this.project = project; this.highlightedText = highlightedText; @@ -93,23 +82,15 @@ public class ChatMessageResponseBody extends JPanel { setLayout(new BoxLayout(this, BoxLayout.Y_AXIS)); setOpaque(false); + if (withProgress) { + add(progressPanel); + } + if (webSearchIncluded) { webpageListPanel = createWebpageListPanel(webpageList); add(webpageListPanel); } - if (webDocIncluded) { - webDocProgressContainer.setLayout(new BoxLayout(webDocProgressContainer, BoxLayout.Y_AXIS)); - webDocProgressContainer.setBorder(JBUI.Borders.emptyBottom(8)); - add(webDocProgressContainer); - } - - if (fileContextIncluded) { - progressContainer.setLayout(new BoxLayout(progressContainer, BoxLayout.Y_AXIS)); - progressContainer.setBorder(JBUI.Borders.emptyBottom(8)); - add(progressContainer); - } - if (withGhostText) { prepareProcessingText(!readOnly); currentlyProcessedTextPane.setText( @@ -224,7 +205,7 @@ public class ChatMessageResponseBody extends JPanel { case ANALYZE_WEB_DOC_STARTED -> showWebDocsProgress(); case ANALYZE_WEB_DOC_COMPLETED -> completeWebDocsProgress(event.getDetails()); case ANALYZE_WEB_DOC_FAILED -> failWebDocsProgress(event.getDetails()); - case PROCESS_CONTEXT -> showProcessContextEvent(event.getDetails()); + case PROCESS_CONTEXT -> progressPanel.updateProgressDetails(event.getDetails()); default -> { } } @@ -315,78 +296,26 @@ public class ChatMessageResponseBody extends JPanel { } private void showWebDocsProgress() { - var wrapper = new JPanel(new FlowLayout(FlowLayout.LEADING, 0, 0)); - wrapper.add(webDocsSpinner); - wrapper.add(Box.createHorizontalStrut(4)); - wrapper.add(new JBLabel( - CodeGPTBundle.get("chatMessageResponseBody.webDocs.startProgress.label")).withFont( - JBFont.small())); - updateWebDocsProgress(wrapper); + progressPanel.updateProgressContainer( + CodeGPTBundle.get("chatMessageResponseBody.webDocs.startProgress.label"), + null + ); } private void completeWebDocsProgress(EventDetails eventDetails) { if (eventDetails instanceof AnalysisCompletedEventDetails defaultEventDetails) { - updateWebDocsProgressLabel(defaultEventDetails.getDescription(), Icons.GreenCheckmark); + progressPanel.updateProgressContainer( + defaultEventDetails.getDescription(), + Icons.GreenCheckmark); } } private void failWebDocsProgress(EventDetails eventDetails) { if (eventDetails instanceof AnalysisFailedEventDetails failedEventDetails) { - updateWebDocsProgressLabel(failedEventDetails.getError(), General.Error); + progressPanel.updateProgressContainer(failedEventDetails.getError(), General.Error); } } - private void showProcessContextEvent(EventDetails eventDetails) { - if (eventDetails instanceof ProcessContextEventDetails details) { - switch (details.getStatus()) { - case "STARTED": { - updateProgressContainer(details.getDescription(), null); - break; - } - case "FAILED": { - updateProgressContainer(details.getDescription(), General.Error); - break; - } - case "COMPLETED": { - updateProgressContainer(details.getDescription(), Icons.GreenCheckmark); - break; - } - default: - break; - } - } - } - - private void updateWebDocsProgressLabel(String text, Icon icon) { - updateWebDocsProgress(new JBLabel(text, icon, SwingConstants.LEADING).withFont(JBFont.small())); - } - - private void updateProgressContainer(String text, @Nullable Icon icon) { - ApplicationManager.getApplication().invokeLater(() -> { - progressContainer.removeAll(); - JComponent wrapper; - if (icon != null) { - wrapper = new JBLabel(text, icon, SwingConstants.LEADING); - ((JBLabel) wrapper).setHorizontalTextPosition(SwingConstants.LEADING); - } else { - wrapper = new JPanel(new FlowLayout(FlowLayout.LEADING, 0, 0)); - wrapper.add(new JBLabel(text)); - wrapper.add(Box.createHorizontalStrut(4)); - wrapper.add(processSpinner); - } - progressContainer.add(JBUI.Panels.simplePanel(wrapper)); - progressContainer.revalidate(); - progressContainer.repaint(); - }); - } - - 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())) { 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 c7a7775d..b8c8d808 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 @@ -99,15 +99,7 @@ public class UserMessagePanel extends JPanel { Project project, String prompt, Disposable parentDisposable) { - return new ChatMessageResponseBody( - project, - null, - false, - true, - false, - false, - false, - parentDisposable) + return new ChatMessageResponseBody(project, null, false, true, false, false, parentDisposable) .withResponse(prompt); } diff --git a/src/main/kotlin/ee/carlrobert/codegpt/toolwindow/ui/ResponseBodyProgressPanel.kt b/src/main/kotlin/ee/carlrobert/codegpt/toolwindow/ui/ResponseBodyProgressPanel.kt new file mode 100644 index 00000000..409d1b38 --- /dev/null +++ b/src/main/kotlin/ee/carlrobert/codegpt/toolwindow/ui/ResponseBodyProgressPanel.kt @@ -0,0 +1,71 @@ +package ee.carlrobert.codegpt.toolwindow.ui + +import com.intellij.icons.AllIcons +import com.intellij.openapi.application.runInEdt +import com.intellij.openapi.diagnostic.thisLogger +import com.intellij.ui.components.JBLabel +import com.intellij.util.ui.AsyncProcessIcon +import com.intellij.util.ui.JBUI +import ee.carlrobert.codegpt.Icons +import ee.carlrobert.codegpt.events.EventDetails +import ee.carlrobert.codegpt.events.ProcessContextEventDetails +import java.awt.FlowLayout +import javax.swing.* + +class ResponseBodyProgressPanel : JPanel() { + + companion object { + private val logger = thisLogger() + } + + private val processSpinner = AsyncProcessIcon("process_spinner") + + init { + layout = BoxLayout(this, BoxLayout.Y_AXIS) + border = JBUI.Borders.emptyBottom(8) + } + + fun updateProgressContainer(text: String, icon: Icon?) { + runInEdt { + removeAll() + val wrapper: JComponent + if (icon != null) { + wrapper = JBLabel(text, icon, SwingConstants.LEADING) + wrapper.horizontalTextPosition = SwingConstants.LEADING + } else { + wrapper = JPanel(FlowLayout(FlowLayout.LEADING, 0, 0)) + wrapper.add(JBLabel(text)) + wrapper.add(Box.createHorizontalStrut(4)) + wrapper.add(processSpinner) + } + add(JBUI.Panels.simplePanel(wrapper)) + revalidate() + repaint() + } + } + + fun updateProgressDetails(eventDetails: EventDetails?) { + if (eventDetails == null) { + logger.error("No event details provided") + return + } + + if (eventDetails is ProcessContextEventDetails) { + when (eventDetails.status) { + "STARTED" -> { + updateProgressContainer(eventDetails.description, null) + } + + "FAILED" -> { + updateProgressContainer(eventDetails.description, AllIcons.General.Error) + } + + "COMPLETED" -> { + updateProgressContainer(eventDetails.description, Icons.GreenCheckmark) + } + + else -> {} + } + } + } +}