Clean up BaseChatToolWindowTabPanel code

This commit is contained in:
Carl-Robert Linnupuu 2023-11-09 03:11:50 +02:00
parent bfd50b18ad
commit c3da76f2bd
16 changed files with 450 additions and 400 deletions

View file

@ -3,35 +3,30 @@ package ee.carlrobert.codegpt.toolwindow.chat;
import static com.intellij.openapi.ui.Messages.OK;
import static ee.carlrobert.codegpt.util.ThemeUtils.getPanelBackgroundColor;
import static java.lang.String.format;
import static java.util.stream.Collectors.toList;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.editor.impl.EditorImpl;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.roots.ui.componentsList.components.ScrollablePanel;
import com.intellij.openapi.roots.ui.componentsList.layout.VerticalStackLayout;
import com.intellij.ui.JBColor;
import com.intellij.ui.ScrollPaneFactory;
import com.intellij.ui.components.JBCheckBox;
import com.intellij.ui.components.JBScrollPane;
import com.intellij.util.messages.MessageBusConnection;
import com.intellij.util.ui.JBUI;
import com.intellij.util.ui.JBUI.Borders;
import ee.carlrobert.codegpt.CodeGPTBundle;
import ee.carlrobert.codegpt.actions.ActionType;
import ee.carlrobert.codegpt.completions.CompletionRequestHandler;
import ee.carlrobert.codegpt.completions.llama.LlamaModel;
import ee.carlrobert.codegpt.completions.you.YouSerpResult;
import ee.carlrobert.codegpt.completions.CompletionRequestService;
import ee.carlrobert.codegpt.completions.ToolWindowCompletionEventListener;
import ee.carlrobert.codegpt.completions.you.YouSubscriptionNotifier;
import ee.carlrobert.codegpt.completions.you.YouUserManager;
import ee.carlrobert.codegpt.completions.you.auth.SignedOutNotifier;
import ee.carlrobert.codegpt.conversations.Conversation;
import ee.carlrobert.codegpt.conversations.ConversationService;
import ee.carlrobert.codegpt.conversations.message.Message;
import ee.carlrobert.codegpt.credentials.AzureCredentialsManager;
import ee.carlrobert.codegpt.credentials.OpenAICredentialsManager;
import ee.carlrobert.codegpt.settings.service.ServiceType;
import ee.carlrobert.codegpt.settings.state.AzureSettingsState;
import ee.carlrobert.codegpt.settings.state.LlamaSettingsState;
import ee.carlrobert.codegpt.settings.state.OpenAISettingsState;
import ee.carlrobert.codegpt.settings.state.SettingsState;
import ee.carlrobert.codegpt.settings.state.YouSettingsState;
@ -42,10 +37,13 @@ import ee.carlrobert.codegpt.toolwindow.chat.components.ResponsePanel;
import ee.carlrobert.codegpt.toolwindow.chat.components.SmartScroller;
import ee.carlrobert.codegpt.toolwindow.chat.components.UserMessagePanel;
import ee.carlrobert.codegpt.toolwindow.chat.components.UserPromptTextArea;
import ee.carlrobert.codegpt.toolwindow.chat.components.YouProCheckbox;
import ee.carlrobert.codegpt.util.EditorUtils;
import ee.carlrobert.codegpt.util.OverlayUtils;
import ee.carlrobert.codegpt.util.SwingUtils;
import ee.carlrobert.codegpt.util.file.FileUtils;
import ee.carlrobert.llm.client.openai.completion.ErrorDetails;
import ee.carlrobert.llm.client.you.completion.YouSerpResult;
import java.awt.BorderLayout;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
@ -57,6 +55,7 @@ import java.util.UUID;
import javax.swing.BoxLayout;
import javax.swing.JComponent;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTextPane;
import javax.swing.ScrollPaneConstants;
import javax.swing.SwingUtilities;
@ -67,6 +66,9 @@ public abstract class BaseChatToolWindowTabPanel implements ChatToolWindowTabPan
private static final Logger LOG = Logger.getInstance(BaseChatToolWindowTabPanel.class);
private final SettingsState settings;
private final YouUserManager youUserManager;
private final boolean useContextualSearch;
private final JPanel rootPanel;
private final ScrollablePanel scrollablePanel;
@ -85,11 +87,15 @@ public abstract class BaseChatToolWindowTabPanel implements ChatToolWindowTabPan
this.project = project;
this.useContextualSearch = useContextualSearch;
this.conversationService = ConversationService.getInstance();
this.rootPanel = new JPanel(new GridBagLayout());
this.scrollablePanel = new ScrollablePanel();
this.scrollablePanel = new ScrollablePanel(new VerticalStackLayout());
this.userPromptTextArea = new UserPromptTextArea(this::handleSubmit);
this.gpt4CheckBox = createGPT4ModelCheckBox();
init();
this.gpt4CheckBox = new YouProCheckbox(project);
this.settings = SettingsState.getInstance();
this.youUserManager = YouUserManager.getInstance();
this.rootPanel = createRootPanel();
userPromptTextArea.requestFocusInWindow();
userPromptTextArea.requestFocus();
}
public void requestFocusForTextArea() {
@ -115,31 +121,14 @@ public abstract class BaseChatToolWindowTabPanel implements ChatToolWindowTabPan
public void displayLandingView() {
scrollablePanel.removeAll();
scrollablePanel.add(getLandingView());
var youUserManager = YouUserManager.getInstance();
if (SettingsState.getInstance().getSelectedService() == ServiceType.YOU &&
if (settings.getSelectedService() == ServiceType.YOU &&
(!youUserManager.isAuthenticated() || !youUserManager.isSubscribed())) {
scrollablePanel.add(new ResponsePanel().addContent(createTextPane()));
scrollablePanel.add(new ResponsePanel().addContent(createYouCouponTextPane()));
}
scrollablePanel.repaint();
scrollablePanel.revalidate();
}
private JTextPane createTextPane() {
var textPane = SwingUtils.createTextPane(SwingUtils::handleHyperlinkClicked);
textPane.setBackground(getPanelBackgroundColor());
textPane.setFocusable(false);
textPane.setText(
"<html>\n"
+ "<body>\n"
+ " <p style=\"margin: 4px 0;\">Use CodeGPT coupon for free month of GPT-4.</p>\n"
+ " <p style=\"margin: 4px 0;\">\n"
+ " <a href=\"https://you.com/plans\">Sign up here</a>\n"
+ " </p>\n"
+ "</body>\n"
+ "</html>");
return textPane;
}
@Override
public void startNewConversation(Message message) {
conversation = conversationService.startConversation();
@ -160,9 +149,10 @@ public abstract class BaseChatToolWindowTabPanel implements ChatToolWindowTabPan
this));
var responsePanel = new ResponsePanel()
.withReloadAction(() -> reloadMessage(message, conversation))
.withDeleteAction(() -> deleteMessage(message.getId(), messageWrapper, conversation))
.withDeleteAction(() -> removeMessage(message.getId(), messageWrapper, conversation))
.addContent(new ChatMessageResponseBody(project, true, this));
messageWrapper.add(responsePanel);
call(conversation, message, responsePanel, false);
}
@ -170,106 +160,6 @@ public abstract class BaseChatToolWindowTabPanel implements ChatToolWindowTabPan
public void dispose() {
}
private boolean isRequestAllowed() {
var settings = SettingsState.getInstance();
if (settings.getSelectedService() == ServiceType.AZURE) {
return AzureCredentialsManager.getInstance().isCredentialSet();
}
if (settings.getSelectedService() == ServiceType.OPENAI) {
return OpenAICredentialsManager.getInstance().isApiKeySet();
}
return true;
}
private void call(
Conversation conversation,
Message message,
ResponsePanel responsePanel,
boolean isRetry) {
ChatMessageResponseBody responseContainer = (ChatMessageResponseBody) responsePanel.getContent();
if (!isRequestAllowed()) {
responseContainer.displayMissingCredential();
return;
}
var requestHandler = new CompletionRequestHandler();
requestHandler.withContextualSearch(useContextualSearch);
requestHandler.addMessageListener(partialMessage -> {
try {
LOG.debug(partialMessage);
ApplicationManager.getApplication()
.invokeLater(() -> responseContainer.update(partialMessage));
} catch (Exception e) {
responseContainer.displayDefaultError();
throw new RuntimeException("Error while updating the content", e);
}
});
requestHandler.addRequestCompletedListener(completeMessage -> {
responsePanel.enableActions();
conversationService.saveMessage(completeMessage, message, conversation, isRetry);
stopStreaming(responseContainer);
var serpResults = serpResultsMapping.get(message.getId());
var containsResults = serpResults != null && !serpResults.isEmpty();
if (YouSettingsState.getInstance().isDisplayWebSearchResults()) {
if (containsResults) {
responseContainer.displaySerpResults(serpResults);
}
}
if (containsResults) {
message.setSerpResults(serpResults.stream()
.map(result -> new YouSerpResult(
result.getUrl(),
result.getName(),
result.getSnippet(),
result.getSnippetSource()))
.collect(toList()));
}
});
requestHandler.addTokensExceededListener(() -> SwingUtilities.invokeLater(() -> {
var answer = OverlayUtils.showTokenLimitExceededDialog();
if (answer == OK) {
TelemetryAction.IDE_ACTION.createActionMessage()
.property("action", "DISCARD_TOKEN_LIMIT")
.property("model", conversation.getModel())
.send();
conversationService.discardTokenLimits(conversation);
requestHandler.call(conversation, message, true);
} else {
stopStreaming(responseContainer);
}
}));
requestHandler.addErrorListener((error, ex) -> {
try {
if ("insufficient_quota".equals(error.getCode())) {
if (SettingsState.getInstance().getSelectedService() == ServiceType.OPENAI) {
OpenAISettingsState.getInstance().setOpenAIQuotaExceeded(true);
}
responseContainer.displayQuotaExceeded();
} else {
responseContainer.displayError(error.getMessage());
}
} finally {
responsePanel.enableActions();
stopStreaming(responseContainer);
}
});
requestHandler.addSerpResultsListener(
serpResults -> serpResultsMapping.put(message.getId(), serpResults.stream()
.map(result -> new YouSerpResult(
result.getUrl(),
result.getName(),
result.getSnippet(),
result.getSnippetSource()))
.collect(toList())));
userPromptTextArea.setRequestHandler(requestHandler);
userPromptTextArea.setSubmitEnabled(false);
requestHandler.call(conversation, message, isRetry);
}
protected void reloadMessage(Message message, Conversation conversation) {
ResponsePanel responsePanel = null;
try {
@ -281,8 +171,10 @@ public abstract class BaseChatToolWindowTabPanel implements ChatToolWindowTabPan
scrollablePanel.revalidate();
scrollablePanel.repaint();
} catch (Exception e) {
throw new RuntimeException("Couldn't reload message", e);
throw new RuntimeException("Couldn't delete the existing message component", e);
} finally {
LOG.debug("Reloading message: " + message.getId());
if (responsePanel != null) {
message.setResponse("");
conversationService.saveMessage(conversation, message);
@ -295,7 +187,7 @@ public abstract class BaseChatToolWindowTabPanel implements ChatToolWindowTabPan
}
}
protected void deleteMessage(UUID messageId, JPanel messageWrapper, Conversation conversation) {
protected void removeMessage(UUID messageId, JPanel messageWrapper, Conversation conversation) {
scrollablePanel.remove(messageWrapper);
scrollablePanel.repaint();
scrollablePanel.revalidate();
@ -327,11 +219,24 @@ public abstract class BaseChatToolWindowTabPanel implements ChatToolWindowTabPan
scrollablePanel.repaint();
}
private void stopStreaming(ChatMessageResponseBody responseContainer) {
SwingUtilities.invokeLater(() -> {
userPromptTextArea.setSubmitEnabled(true);
responseContainer.hideCarets();
});
private void call(
Conversation conversation,
Message message,
ResponsePanel responsePanel,
boolean isRetry) {
ChatMessageResponseBody responseContainer = (ChatMessageResponseBody) responsePanel.getContent();
if (!CompletionRequestService.getInstance().isRequestAllowed()) {
responseContainer.displayMissingCredential();
return;
}
var requestHandler = new CompletionRequestHandler(
useContextualSearch,
new ChatToolWindowCompletionEventListener(responsePanel));
userPromptTextArea.setRequestHandler(requestHandler);
userPromptTextArea.setSubmitEnabled(false);
requestHandler.call(conversation, message, isRetry);
}
private void handleSubmit(String text) {
@ -356,46 +261,44 @@ public abstract class BaseChatToolWindowTabPanel implements ChatToolWindowTabPan
}
}
private void init() {
private static JScrollPane createScrollPane(ScrollablePanel scrollablePanel) {
var scrollPane = ScrollPaneFactory.createScrollPane(scrollablePanel, true);
scrollPane.setHorizontalScrollBarPolicy(ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER);
new SmartScroller(scrollPane);
return scrollPane;
}
private JPanel createRootPanel() {
var rootPanel = new JPanel(new GridBagLayout());
var gbc = new GridBagConstraints();
gbc.fill = GridBagConstraints.BOTH;
gbc.weighty = 1;
gbc.weightx = 1;
gbc.gridx = 0;
gbc.gridy = 0;
scrollablePanel.setLayout(new BoxLayout(scrollablePanel, BoxLayout.Y_AXIS));
JBScrollPane scrollPane = new JBScrollPane(scrollablePanel);
scrollPane.setHorizontalScrollBarPolicy(ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER);
scrollPane.setBorder(null);
scrollPane.setViewportBorder(null);
rootPanel.add(scrollPane, gbc);
new SmartScroller(scrollPane);
gbc.weighty = 0;
gbc.fill = GridBagConstraints.HORIZONTAL;
gbc.gridy = 1;
var model = getModel();
var modelIconWrapper = JBUI.Panels
.simplePanel(new ModelIconLabel(
SettingsState.getInstance()
.getSelectedService()
.getCompletionCode(),
model))
.withBorder(Borders.emptyRight(4))
.withBackground(getPanelBackgroundColor());
rootPanel.add(createScrollPane(scrollablePanel), gbc);
var wrapper = new JPanel(new BorderLayout());
wrapper.setBorder(JBUI.Borders.compound(
JBUI.Borders.customLine(JBColor.border(), 1, 0, 0, 0),
JBUI.Borders.empty(8)));
wrapper.setBackground(getPanelBackgroundColor());
wrapper.add(createPromptTextAreaHeader(), BorderLayout.NORTH);
wrapper.add(userPromptTextArea, BorderLayout.SOUTH);
gbc.weighty = 0;
gbc.fill = GridBagConstraints.HORIZONTAL;
gbc.gridy = 1;
rootPanel.add(wrapper, gbc);
return rootPanel;
}
private JPanel createPromptTextAreaHeader() {
var header = new JPanel(new BorderLayout());
header.setBackground(getPanelBackgroundColor());
header.setBorder(JBUI.Borders.emptyBottom(8));
var model = settings.getModel();
if ("YouCode".equals(model)) {
var messageBusConnection = ApplicationManager.getApplication().getMessageBus().connect();
subscribeToYouModelChangeTopic();
@ -403,18 +306,13 @@ public abstract class BaseChatToolWindowTabPanel implements ChatToolWindowTabPan
subscribeToSignedOutTopic(messageBusConnection);
header.add(gpt4CheckBox, BorderLayout.LINE_START);
}
header.add(modelIconWrapper, BorderLayout.LINE_END);
wrapper.add(header);
rootPanel.add(wrapper, gbc);
userPromptTextArea.requestFocusInWindow();
userPromptTextArea.requestFocus();
}
private void subscribeToSignedOutTopic(MessageBusConnection messageBusConnection) {
messageBusConnection.subscribe(
SignedOutNotifier.SIGNED_OUT_TOPIC,
(SignedOutNotifier) () -> gpt4CheckBox.setEnabled(false));
header.add(JBUI.Panels
.simplePanel(
new ModelIconLabel(settings.getSelectedService().getCompletionCode(),
model))
.withBorder(Borders.emptyRight(4))
.withBackground(getPanelBackgroundColor()), BorderLayout.LINE_END);
return header;
}
private void subscribeToYouModelChangeTopic() {
@ -425,6 +323,12 @@ public abstract class BaseChatToolWindowTabPanel implements ChatToolWindowTabPan
(YouModelChangeNotifier) gpt4CheckBox::setSelected);
}
private void subscribeToSignedOutTopic(MessageBusConnection messageBusConnection) {
messageBusConnection.subscribe(
SignedOutNotifier.SIGNED_OUT_TOPIC,
(SignedOutNotifier) () -> gpt4CheckBox.setEnabled(false));
}
private void subscribeToYouSubscriptionTopic(MessageBusConnection messageBusConnection) {
messageBusConnection.subscribe(
YouSubscriptionNotifier.SUBSCRIPTION_TOPIC,
@ -434,64 +338,111 @@ public abstract class BaseChatToolWindowTabPanel implements ChatToolWindowTabPan
});
}
private JBCheckBox createGPT4ModelCheckBox() {
var gpt4CheckBox = new JBCheckBox(CodeGPTBundle.get("toolwindow.chat.youProCheckBox.text"));
gpt4CheckBox.setOpaque(false);
gpt4CheckBox.setEnabled(YouUserManager.getInstance().isSubscribed());
gpt4CheckBox.setSelected(YouSettingsState.getInstance().isUseGPT4Model());
gpt4CheckBox.setToolTipText(getTooltipText(gpt4CheckBox.isSelected()));
gpt4CheckBox.addChangeListener(e -> {
var selected = ((JBCheckBox) e.getSource()).isSelected();
var tooltipText = getTooltipText(selected);
gpt4CheckBox.setToolTipText(tooltipText);
// TODO: Remove
project.getMessageBus()
.syncPublisher(YouModelChangeNotifier.YOU_MODEL_CHANGE_NOTIFIER_TOPIC)
.modelChanged(selected);
YouSettingsState.getInstance().setUseGPT4Model(selected);
});
return gpt4CheckBox;
private JTextPane createYouCouponTextPane() {
var textPane = SwingUtils.createTextPane(
"<html>\n"
+ "<body>\n"
+ " <p style=\"margin: 4px 0;\">Use CodeGPT coupon for free month of GPT-4.</p>\n"
+ " <p style=\"margin: 4px 0;\">\n"
+ " <a href=\"https://you.com/plans\">Sign up here</a>\n"
+ " </p>\n"
+ "</body>\n"
+ "</html>"
);
textPane.setBackground(getPanelBackgroundColor());
textPane.setFocusable(false);
return textPane;
}
private String getTooltipText(boolean selected) {
if (YouUserManager.getInstance().isSubscribed()) {
return selected ?
CodeGPTBundle.get("toolwindow.chat.youProCheckBox.disable") :
CodeGPTBundle.get("toolwindow.chat.youProCheckBox.enable");
}
return CodeGPTBundle.get("toolwindow.chat.youProCheckBox.notAllowed");
}
private class ChatToolWindowCompletionEventListener implements ToolWindowCompletionEventListener {
private String getModel() {
var settings = SettingsState.getInstance();
if (settings.getSelectedService() == ServiceType.OPENAI) {
return OpenAISettingsState.getInstance().getModel();
private final Logger LOG = Logger.getInstance(ChatToolWindowCompletionEventListener.class);
private final ResponsePanel responsePanel;
private final ChatMessageResponseBody responseContainer;
public ChatToolWindowCompletionEventListener(ResponsePanel responsePanel) {
this.responsePanel = responsePanel;
this.responseContainer = (ChatMessageResponseBody) responsePanel.getContent();
}
if (settings.getSelectedService() == ServiceType.AZURE) {
return AzureSettingsState.getInstance().getModel();
}
if (settings.getSelectedService() == ServiceType.YOU) {
return "YouCode";
}
if (settings.getSelectedService() == ServiceType.LLAMA_CPP) {
var llamaSettings = LlamaSettingsState.getInstance();
if (llamaSettings.isUseCustomModel()) {
var filePath = llamaSettings.getCustomLlamaModelPath();
int lastSeparatorIndex = filePath.lastIndexOf('/');
if (lastSeparatorIndex == -1) {
return filePath;
}
return filePath.substring(lastSeparatorIndex + 1);
@Override
public void handleMessage(String message) {
try {
LOG.debug(message);
ApplicationManager.getApplication()
.invokeLater(() -> responseContainer.update(message));
} catch (Exception e) {
responseContainer.displayDefaultError();
throw new RuntimeException("Error while updating the content", e);
}
var huggingFaceModel = llamaSettings.getHuggingFaceModel();
var llamaModel = LlamaModel.findByHuggingFaceModel(huggingFaceModel);
return String.format(
"%s %dB (Q%d)",
llamaModel.getLabel(),
huggingFaceModel.getParameterSize(),
huggingFaceModel.getQuantization());
}
return "Unknown";
@Override
public void handleError(ErrorDetails error, Throwable ex) {
try {
if ("insufficient_quota".equals(error.getCode())) {
if (SettingsState.getInstance().getSelectedService() == ServiceType.OPENAI) {
OpenAISettingsState.getInstance().setOpenAIQuotaExceeded(true);
}
responseContainer.displayQuotaExceeded();
} else {
responseContainer.displayError(error.getMessage());
}
} finally {
LOG.error(error.getMessage(), ex);
responsePanel.enableActions();
stopStreaming(responseContainer);
}
}
@Override
public void handleTokensExceeded(Conversation conversation, Message message) {
var answer = OverlayUtils.showTokenLimitExceededDialog();
if (answer == OK) {
TelemetryAction.IDE_ACTION.createActionMessage()
.property("action", "DISCARD_TOKEN_LIMIT")
.property("model", conversation.getModel())
.send();
conversationService.discardTokenLimits(conversation);
call(conversation, message, responsePanel, true);
} else {
stopStreaming(responseContainer);
}
}
@Override
public void handleCompleted(
String fullMessage,
Message message,
Conversation conversation,
boolean isRetry) {
responsePanel.enableActions();
conversationService.saveMessage(fullMessage, message, conversation, isRetry);
stopStreaming(responseContainer);
var serpResults = serpResultsMapping.get(message.getId());
var containsResults = serpResults != null && !serpResults.isEmpty();
if (YouSettingsState.getInstance().isDisplayWebSearchResults() && containsResults) {
responseContainer.displaySerpResults(serpResults);
}
if (containsResults) {
message.setSerpResults(serpResults);
}
}
@Override
public void handleSerpResults(List<YouSerpResult> results, Message message) {
serpResultsMapping.put(message.getId(), results);
}
private void stopStreaming(ChatMessageResponseBody responseContainer) {
SwingUtilities.invokeLater(() -> {
userPromptTextArea.setSubmitEnabled(true);
responseContainer.hideCarets();
});
}
}
}