feat: improve tool window's textbox (#621)

* feat: initial smart user input panel implementation

* refactor: clean up
This commit is contained in:
Carl-Robert 2024-07-18 14:18:51 +03:00 committed by GitHub
parent 5baab54697
commit 1fc47fa889
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
15 changed files with 965 additions and 462 deletions

View file

@ -1,7 +1,14 @@
package ee.carlrobert.codegpt.settings.service;
import static ee.carlrobert.llm.client.openai.completion.OpenAIChatCompletionModel.GPT_4_O;
import static ee.carlrobert.llm.client.openai.completion.OpenAIChatCompletionModel.GPT_4_VISION_PREVIEW;
import com.intellij.openapi.application.ApplicationManager;
import ee.carlrobert.codegpt.CodeGPTBundle;
import ee.carlrobert.codegpt.settings.service.codegpt.CodeGPTServiceSettings;
import ee.carlrobert.codegpt.settings.service.openai.OpenAISettings;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public enum ServiceType {
@ -56,4 +63,26 @@ public enum ServiceType {
}
return serviceType;
}
public boolean isImageActionSupported() {
return switch (this) {
case ANTHROPIC:
case OLLAMA:
yield true;
case CODEGPT:
var codegptModel = ApplicationManager.getApplication()
.getService(CodeGPTServiceSettings.class)
.getState()
.getChatCompletionSettings()
.getModel();
yield List.of("gpt-4o", "claude-3-opus").contains(codegptModel);
case OPENAI:
var openaiModel = ApplicationManager.getApplication().getService(OpenAISettings.class)
.getState()
.getModel();
yield List.of(GPT_4_VISION_PREVIEW.getCode(), GPT_4_O.getCode()).contains(openaiModel);
default:
yield false;
};
}
}

View file

@ -64,7 +64,7 @@ public class ChatToolWindowPanel extends SimpleToolWindowPanel {
var messageBusConnection = project.getMessageBus().connect();
messageBusConnection.subscribe(IncludeFilesInContextNotifier.FILES_INCLUDED_IN_CONTEXT_TOPIC,
(IncludeFilesInContextNotifier) this::displaySelectedFilesNotification);
(IncludeFilesInContextNotifier) this::updateSelectedFilesNotification);
messageBusConnection.subscribe(AttachImageNotifier.IMAGE_ATTACHMENT_FILE_PATH_TOPIC,
(AttachImageNotifier) filePath -> imageFileAttachmentNotification.show(
Path.of(filePath).getFileName().toString(),
@ -95,8 +95,9 @@ public class ChatToolWindowPanel extends SimpleToolWindowPanel {
return tabbedPane;
}
public void displaySelectedFilesNotification(List<ReferencedFile> referencedFiles) {
public void updateSelectedFilesNotification(List<ReferencedFile> referencedFiles) {
if (referencedFiles.isEmpty()) {
selectedFilesNotification.hideNotification();
return;
}

View file

@ -5,12 +5,10 @@ import static ee.carlrobert.codegpt.ui.UIUtil.createScrollPaneWithSmartScroller;
import static java.lang.String.format;
import com.intellij.openapi.Disposable;
import com.intellij.openapi.actionSystem.ActionPlaces;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.project.Project;
import com.intellij.ui.JBColor;
import com.intellij.util.ui.JBUI;
import com.intellij.util.ui.JBUI.Borders;
import ee.carlrobert.codegpt.CodeGPTKeys;
import ee.carlrobert.codegpt.EncodingManager;
import ee.carlrobert.codegpt.ReferencedFile;
@ -22,19 +20,16 @@ import ee.carlrobert.codegpt.completions.ConversationType;
import ee.carlrobert.codegpt.conversations.Conversation;
import ee.carlrobert.codegpt.conversations.ConversationService;
import ee.carlrobert.codegpt.conversations.message.Message;
import ee.carlrobert.codegpt.settings.GeneralSettings;
import ee.carlrobert.codegpt.settings.service.ServiceType;
import ee.carlrobert.codegpt.telemetry.TelemetryAction;
import ee.carlrobert.codegpt.toolwindow.chat.ui.ChatMessageResponseBody;
import ee.carlrobert.codegpt.toolwindow.chat.ui.ChatToolWindowScrollablePanel;
import ee.carlrobert.codegpt.toolwindow.chat.ui.ResponsePanel;
import ee.carlrobert.codegpt.toolwindow.chat.ui.UserMessagePanel;
import ee.carlrobert.codegpt.toolwindow.chat.ui.textarea.ModelComboBoxAction;
import ee.carlrobert.codegpt.toolwindow.chat.ui.textarea.TotalTokensDetails;
import ee.carlrobert.codegpt.toolwindow.chat.ui.textarea.TotalTokensPanel;
import ee.carlrobert.codegpt.toolwindow.chat.ui.textarea.UserPromptTextArea;
import ee.carlrobert.codegpt.toolwindow.ui.ChatToolWindowLandingPanel;
import ee.carlrobert.codegpt.ui.OverlayUtil;
import ee.carlrobert.codegpt.ui.textarea.UserInputPanel;
import ee.carlrobert.codegpt.util.EditorUtil;
import ee.carlrobert.codegpt.util.file.FileUtil;
import java.awt.BorderLayout;
@ -44,7 +39,6 @@ import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.UUID;
import java.util.function.Consumer;
import javax.swing.JComponent;
import javax.swing.JPanel;
import javax.swing.SwingUtilities;
@ -59,11 +53,13 @@ public class ChatToolWindowTabPanel implements Disposable {
private final Project project;
private final JPanel rootPanel;
private final Conversation conversation;
private final UserPromptTextArea userPromptTextArea;
private final UserInputPanel textArea;
private final ConversationService conversationService;
private final TotalTokensPanel totalTokensPanel;
private final ChatToolWindowScrollablePanel toolWindowScrollablePanel;
private @Nullable CompletionRequestHandler requestHandler;
public ChatToolWindowTabPanel(@NotNull Project project, @NotNull Conversation conversation) {
this.project = project;
this.conversation = conversation;
@ -74,10 +70,9 @@ public class ChatToolWindowTabPanel implements Disposable {
conversation,
EditorUtil.getSelectedEditorSelectedText(project),
this);
userPromptTextArea = new UserPromptTextArea(this::handleSubmit, totalTokensPanel);
textArea = new UserInputPanel(project, this::handleSubmit, this::handleCancel);
textArea.requestFocus();
rootPanel = createRootPanel();
userPromptTextArea.requestFocusInWindow();
userPromptTextArea.requestFocus();
if (conversation.getMessages().isEmpty()) {
displayLandingView();
@ -103,7 +98,7 @@ public class ChatToolWindowTabPanel implements Disposable {
}
public void requestFocusForTextArea() {
userPromptTextArea.focus();
textArea.requestFocus();
}
public void displayLandingView() {
@ -233,24 +228,23 @@ public class ChatToolWindowTabPanel implements Disposable {
return;
}
var requestHandler = new CompletionRequestHandler(
requestHandler = new CompletionRequestHandler(
new ToolWindowCompletionResponseEventListener(
conversationService,
responsePanel,
totalTokensPanel,
userPromptTextArea) {
textArea) {
@Override
public void handleTokensExceededPolicyAccepted() {
call(callParameters, responsePanel);
}
});
userPromptTextArea.setRequestHandler(requestHandler);
userPromptTextArea.setSubmitEnabled(false);
textArea.setSubmitEnabled(false);
requestHandler.call(callParameters);
}
private void handleSubmit(String text) {
private Unit handleSubmit(String text) {
var message = new Message(text);
var editor = EditorUtil.getSelectedEditor(project);
if (editor != null) {
@ -264,37 +258,27 @@ public class ChatToolWindowTabPanel implements Disposable {
}
message.setUserMessage(text);
sendMessage(message, ConversationType.DEFAULT);
return Unit.INSTANCE;
}
private JPanel createUserPromptPanel(ServiceType selectedService) {
private Unit handleCancel() {
if (requestHandler != null) {
requestHandler.cancel();
}
return Unit.INSTANCE;
}
private JPanel createUserPromptPanel() {
var panel = new JPanel(new BorderLayout());
panel.setBorder(JBUI.Borders.compound(
JBUI.Borders.customLine(JBColor.border(), 1, 0, 0, 0),
JBUI.Borders.empty(8)));
var contentManager = project.getService(ChatToolWindowContentManager.class);
panel.add(JBUI.Panels.simplePanel(createUserPromptTextAreaHeader(
project,
selectedService,
(provider) -> {
ConversationService.getInstance().startConversation();
contentManager.createNewTabPanel();
})), BorderLayout.NORTH);
panel.add(JBUI.Panels.simplePanel(userPromptTextArea), BorderLayout.CENTER);
panel.add(JBUI.Panels.simplePanel(totalTokensPanel)
.withBorder(JBUI.Borders.emptyBottom(8)), BorderLayout.NORTH);
panel.add(JBUI.Panels.simplePanel(textArea), BorderLayout.CENTER);
return panel;
}
private JPanel createUserPromptTextAreaHeader(
Project project,
ServiceType selectedService,
Consumer<ServiceType> onModelChange) {
return JBUI.Panels.simplePanel()
.withBorder(Borders.emptyBottom(8))
.andTransparent()
.addToLeft(totalTokensPanel)
.addToRight(new ModelComboBoxAction(project, onModelChange, selectedService)
.createCustomComponent(ActionPlaces.UNKNOWN));
}
private JComponent getLandingView() {
return new ChatToolWindowLandingPanel((action, locationOnScreen) -> {
var editor = EditorUtil.getSelectedEditor(project);
@ -354,8 +338,7 @@ public class ChatToolWindowTabPanel implements Disposable {
gbc.weighty = 0;
gbc.fill = GridBagConstraints.HORIZONTAL;
gbc.gridy = 1;
rootPanel.add(
createUserPromptPanel(GeneralSettings.getSelectedService()), gbc);
rootPanel.add(createUserPromptPanel(), gbc);
return rootPanel;
}
}

View file

@ -14,8 +14,8 @@ import ee.carlrobert.codegpt.telemetry.TelemetryAction;
import ee.carlrobert.codegpt.toolwindow.chat.ui.ChatMessageResponseBody;
import ee.carlrobert.codegpt.toolwindow.chat.ui.ResponsePanel;
import ee.carlrobert.codegpt.toolwindow.chat.ui.textarea.TotalTokensPanel;
import ee.carlrobert.codegpt.toolwindow.chat.ui.textarea.UserPromptTextArea;
import ee.carlrobert.codegpt.ui.OverlayUtil;
import ee.carlrobert.codegpt.ui.textarea.UserInputPanel;
import ee.carlrobert.llm.client.openai.completion.ErrorDetails;
import ee.carlrobert.llm.client.you.completion.YouSerpResult;
import java.util.HashMap;
@ -37,7 +37,7 @@ abstract class ToolWindowCompletionResponseEventListener implements
private final ResponsePanel responsePanel;
private final ChatMessageResponseBody responseContainer;
private final TotalTokensPanel totalTokensPanel;
private final UserPromptTextArea userPromptTextArea;
private final UserInputPanel textArea;
private volatile boolean completed;
@ -45,13 +45,13 @@ abstract class ToolWindowCompletionResponseEventListener implements
ConversationService conversationService,
ResponsePanel responsePanel,
TotalTokensPanel totalTokensPanel,
UserPromptTextArea userPromptTextArea) {
UserInputPanel textArea) {
this.encodingManager = EncodingManager.getInstance();
this.conversationService = conversationService;
this.responsePanel = responsePanel;
this.responseContainer = (ChatMessageResponseBody) responsePanel.getContent();
this.totalTokensPanel = totalTokensPanel;
this.userPromptTextArea = userPromptTextArea;
this.textArea = textArea;
}
public abstract void handleTokensExceededPolicyAccepted();
@ -127,7 +127,7 @@ abstract class ToolWindowCompletionResponseEventListener implements
if (containsResults) {
responseContainer.displaySerpResults(serpResults);
}
totalTokensPanel.updateUserPromptTokens(userPromptTextArea.getText());
totalTokensPanel.updateUserPromptTokens(textArea.getText());
totalTokensPanel.updateConversationTokens(callParameters.getConversation());
} finally {
stopStreaming(responseContainer);
@ -142,7 +142,7 @@ abstract class ToolWindowCompletionResponseEventListener implements
private void stopStreaming(ChatMessageResponseBody responseContainer) {
completed = true;
userPromptTextArea.setSubmitEnabled(true);
textArea.setSubmitEnabled(true);
responseContainer.hideCaret();
}
}

View file

@ -37,7 +37,7 @@ public class SelectedFilesAccordion extends JPanel {
private JPanel createContentPanel(Project project, List<String> referencedFilePaths) {
var panel = new JPanel();
panel.setOpaque(false);
panel.setVisible(false);
panel.setVisible(true);
panel.setLayout(new BoxLayout(panel, BoxLayout.Y_AXIS));
panel.setBorder(JBUI.Borders.empty(4, 0));
referencedFilePaths.stream()

View file

@ -1,231 +0,0 @@
package ee.carlrobert.codegpt.toolwindow.chat.ui.textarea;
import static ee.carlrobert.codegpt.settings.service.ServiceType.ANTHROPIC;
import static ee.carlrobert.codegpt.settings.service.ServiceType.CODEGPT;
import static ee.carlrobert.codegpt.settings.service.ServiceType.OLLAMA;
import static ee.carlrobert.codegpt.settings.service.ServiceType.OPENAI;
import static ee.carlrobert.llm.client.openai.completion.OpenAIChatCompletionModel.GPT_4_O;
import static ee.carlrobert.llm.client.openai.completion.OpenAIChatCompletionModel.GPT_4_VISION_PREVIEW;
import com.intellij.icons.AllIcons;
import com.intellij.openapi.actionSystem.AnAction;
import com.intellij.openapi.actionSystem.AnActionEvent;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.editor.ex.util.EditorUtil;
import com.intellij.openapi.util.registry.Registry;
import com.intellij.ui.DocumentAdapter;
import com.intellij.ui.JBColor;
import com.intellij.ui.components.JBTextArea;
import com.intellij.util.ui.JBUI;
import ee.carlrobert.codegpt.CodeGPTBundle;
import ee.carlrobert.codegpt.Icons;
import ee.carlrobert.codegpt.actions.AttachImageAction;
import ee.carlrobert.codegpt.completions.CompletionRequestHandler;
import ee.carlrobert.codegpt.settings.GeneralSettings;
import ee.carlrobert.codegpt.settings.service.codegpt.CodeGPTServiceSettings;
import ee.carlrobert.codegpt.settings.service.openai.OpenAISettings;
import ee.carlrobert.codegpt.ui.IconActionButton;
import ee.carlrobert.codegpt.ui.UIUtil;
import java.awt.BasicStroke;
import java.awt.BorderLayout;
import java.awt.FlowLayout;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Insets;
import java.awt.RenderingHints;
import java.awt.event.ActionEvent;
import java.awt.event.FocusEvent;
import java.awt.event.FocusListener;
import java.util.List;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Consumer;
import javax.swing.AbstractAction;
import javax.swing.JPanel;
import javax.swing.UIManager;
import javax.swing.event.DocumentEvent;
import javax.swing.text.BadLocationException;
import org.jetbrains.annotations.NotNull;
public class UserPromptTextArea extends JPanel {
private static final Logger LOG = Logger.getInstance(UserPromptTextArea.class);
private static final JBColor BACKGROUND_COLOR = JBColor.namedColor(
"Editor.SearchField.background", com.intellij.util.ui.UIUtil.getTextFieldBackground());
private final AtomicReference<CompletionRequestHandler> requestHandlerRef =
new AtomicReference<>();
private final JBTextArea textArea;
private final int textAreaRadius = 16;
private final Consumer<String> onSubmit;
private IconActionButton stopButton;
private boolean submitEnabled = true;
public UserPromptTextArea(Consumer<String> onSubmit, TotalTokensPanel totalTokensPanel) {
super(new BorderLayout());
this.onSubmit = onSubmit;
textArea = new JBTextArea();
textArea.getDocument().addDocumentListener(getDocumentAdapter(totalTokensPanel));
textArea.setOpaque(false);
textArea.setBackground(BACKGROUND_COLOR);
textArea.setLineWrap(true);
textArea.setWrapStyleWord(true);
textArea.getEmptyText().setText(CodeGPTBundle.get("toolwindow.chat.textArea.emptyText"));
textArea.setBorder(JBUI.Borders.empty(8, 4));
UIUtil.addShiftEnterInputMap(textArea, new AbstractAction() {
@Override
public void actionPerformed(ActionEvent e) {
try {
handleSubmit();
} finally {
totalTokensPanel.updateUserPromptTokens("");
}
}
});
textArea.addFocusListener(new FocusListener() {
@Override
public void focusGained(FocusEvent e) {
UserPromptTextArea.super.paintBorder(UserPromptTextArea.super.getGraphics());
}
@Override
public void focusLost(FocusEvent e) {
UserPromptTextArea.super.paintBorder(UserPromptTextArea.super.getGraphics());
}
});
updateFont();
init();
}
private DocumentAdapter getDocumentAdapter(TotalTokensPanel totalTokensPanel) {
return new DocumentAdapter() {
@Override
protected void textChanged(@NotNull DocumentEvent event) {
if (submitEnabled) {
try {
var document = event.getDocument();
var text = document.getText(
document.getStartPosition().getOffset(),
document.getEndPosition().getOffset() - 1);
totalTokensPanel.updateUserPromptTokens(text);
} catch (BadLocationException ex) {
LOG.error("Something went wrong while processing user input tokens", ex);
}
}
}
};
}
public String getText() {
return textArea.getText().trim();
}
public void focus() {
textArea.requestFocus();
textArea.requestFocusInWindow();
}
@Override
protected void paintComponent(Graphics g) {
Graphics2D g2 = (Graphics2D) g.create();
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
g2.setColor(getBackground());
g2.fillRoundRect(0, 0, getWidth() - 1, getHeight() - 1, textAreaRadius, textAreaRadius);
super.paintComponent(g);
}
@Override
protected void paintBorder(Graphics g) {
Graphics2D g2 = (Graphics2D) g.create();
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
g2.setColor(JBUI.CurrentTheme.ActionButton.focusedBorder());
if (textArea.isFocusOwner()) {
g2.setStroke(new BasicStroke(1.5F));
}
g2.drawRoundRect(0, 0, getWidth() - 1, getHeight() - 1, textAreaRadius, textAreaRadius);
}
@Override
public Insets getInsets() {
return JBUI.insets(6, 12, 6, 6);
}
public void setSubmitEnabled(boolean submitEnabled) {
this.submitEnabled = submitEnabled;
stopButton.setEnabled(!submitEnabled);
}
public void setRequestHandler(@NotNull CompletionRequestHandler handler) {
requestHandlerRef.set(handler);
}
private void handleSubmit() {
if (submitEnabled && !textArea.getText().isEmpty()) {
// Replacing each newline with two newlines to ensure proper Markdown formatting
var text = textArea.getText().replace("\n", "\n\n");
onSubmit.accept(text.trim());
textArea.setText("");
}
}
private void init() {
setOpaque(false);
add(textArea, BorderLayout.CENTER);
stopButton = new IconActionButton(
new AnAction("Stop", "Stop current inference", AllIcons.Actions.Suspend) {
@Override
public void actionPerformed(@NotNull AnActionEvent e) {
var handler = requestHandlerRef.get();
if (handler != null) {
handler.cancel();
}
}
});
stopButton.setEnabled(false);
var flowLayout = new FlowLayout(FlowLayout.RIGHT);
flowLayout.setHgap(8);
JPanel iconsPanel = new JPanel(flowLayout);
iconsPanel.add(new IconActionButton(
new AnAction("Send Message", "Send message", Icons.Send) {
@Override
public void actionPerformed(@NotNull AnActionEvent e) {
handleSubmit();
}
}));
if (isImageActionSupported()) {
iconsPanel.add(new IconActionButton(new AttachImageAction()));
}
iconsPanel.add(stopButton);
add(iconsPanel, BorderLayout.EAST);
}
private boolean isImageActionSupported() {
var selectedService = GeneralSettings.getSelectedService();
if (selectedService == ANTHROPIC || selectedService == OLLAMA) {
return true;
}
if (selectedService == CODEGPT) {
var model = ApplicationManager.getApplication().getService(CodeGPTServiceSettings.class)
.getState()
.getChatCompletionSettings()
.getModel();
return List.of("gpt-4o", "claude-3-opus").contains(model);
}
var model = OpenAISettings.getCurrentState().getModel();
return selectedService == OPENAI && (
GPT_4_VISION_PREVIEW.getCode().equals(model) || GPT_4_O.getCode().equals(model));
}
private void updateFont() {
if (Registry.is("ide.find.use.editor.font", false)) {
textArea.setFont(EditorUtil.getEditorFont());
} else {
textArea.setFont(UIManager.getFont("TextField.font"));
}
}
}