mirror of
https://github.com/carlrobertoh/ProxyAI.git
synced 2026-05-20 17:52:23 +00:00
refactor: improve chat response rendering performance
This commit is contained in:
parent
f45f8e2e02
commit
2312a9dcb2
8 changed files with 132 additions and 111 deletions
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -24,4 +24,7 @@ public interface CompletionResponseEventListener {
|
|||
|
||||
default void handleCodeGPTEvent(CodeGPTEvent event) {
|
||||
}
|
||||
|
||||
default void handleRequestOpen() {
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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())) {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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 -> {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue