refactor: improve chat response rendering performance

This commit is contained in:
Carl-Robert Linnupuu 2024-10-18 01:26:57 +03:00
parent f45f8e2e02
commit 2312a9dcb2
8 changed files with 132 additions and 111 deletions

View file

@ -21,6 +21,11 @@ public class ChatCompletionEventListener implements CompletionEventListener<Stri
this.eventListener = eventListener;
}
@Override
public void onOpen() {
eventListener.handleRequestOpen();
}
@Override
public void onEvent(String data) {
try {

View file

@ -232,10 +232,6 @@ public final class CompletionRequestService {
throw new IllegalStateException("Unknown request type: " + request.getClass());
}
public boolean isAllowed() {
return isRequestAllowed();
}
public static boolean isRequestAllowed() {
return isRequestAllowed(GeneralSettings.getSelectedService());
}
@ -247,8 +243,9 @@ public final class CompletionRequestService {
AzureSettings.getCurrentState().isUseAzureApiKeyAuthentication()
? CredentialKey.AZURE_OPENAI_API_KEY
: CredentialKey.AZURE_ACTIVE_DIRECTORY_TOKEN);
case CODEGPT, CUSTOM_OPENAI, ANTHROPIC, LLAMA_CPP, OLLAMA -> 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;
};
}

View file

@ -24,4 +24,7 @@ public interface CompletionResponseEventListener {
default void handleCodeGPTEvent(CodeGPTEvent event) {
}
default void handleRequestOpen() {
}
}

View file

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

View file

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

View file

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

View file

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

View file

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