feat: support copying user and response messages (closes #791)

This commit is contained in:
Carl-Robert Linnupuu 2024-12-17 10:01:59 +00:00
parent 1688394e87
commit ce46deeb57
17 changed files with 463 additions and 373 deletions

View file

@ -22,13 +22,14 @@ import ee.carlrobert.codegpt.conversations.Conversation;
import ee.carlrobert.codegpt.conversations.ConversationService;
import ee.carlrobert.codegpt.conversations.message.Message;
import ee.carlrobert.codegpt.telemetry.TelemetryAction;
import ee.carlrobert.codegpt.toolwindow.chat.editor.actions.CopyAction;
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.TotalTokensDetails;
import ee.carlrobert.codegpt.toolwindow.chat.ui.textarea.TotalTokensPanel;
import ee.carlrobert.codegpt.toolwindow.ui.ChatToolWindowLandingPanel;
import ee.carlrobert.codegpt.toolwindow.ui.ResponseMessagePanel;
import ee.carlrobert.codegpt.toolwindow.ui.UserMessagePanel;
import ee.carlrobert.codegpt.ui.OverlayUtil;
import ee.carlrobert.codegpt.ui.textarea.AppliedActionInlay;
import ee.carlrobert.codegpt.ui.textarea.UserInputPanel;
@ -174,12 +175,14 @@ public class ChatToolWindowTabPanel implements Disposable {
totalTokensPanel.updateReferencedFilesTokens(callParameters.getReferencedFiles());
}
var responsePanel = createResponsePanel(callParameters);
var messagePanel = toolWindowScrollablePanel.addMessage(message.getId());
messagePanel.add(new UserMessagePanel(project, message, this));
messagePanel.add(responsePanel);
var userMessagePanel = createUserMessagePanel(message, callParameters);
var responseMessagePanel = createResponseMessagePanel(callParameters);
call(callParameters, responsePanel);
var messagePanel = toolWindowScrollablePanel.addMessage(message.getId());
messagePanel.add(userMessagePanel);
messagePanel.add(responseMessagePanel);
call(callParameters, responseMessagePanel, userMessagePanel);
});
}
@ -193,29 +196,39 @@ public class ChatToolWindowTabPanel implements Disposable {
it -> it.getReferencedFilePaths() != null && !it.getReferencedFilePaths().isEmpty());
}
private ResponsePanel createResponsePanel(ChatCompletionParameters callParameters) {
private UserMessagePanel createUserMessagePanel(
Message message,
ChatCompletionParameters callParameters) {
var panel = new UserMessagePanel(project, message, this);
panel.addCopyAction(() -> CopyAction.copyToClipboard(message.getPrompt()));
panel.addReloadAction(() -> reloadMessage(callParameters, panel));
panel.addDeleteAction(() -> removeMessage(message.getId(), conversation));
return panel;
}
private ResponseMessagePanel createResponseMessagePanel(ChatCompletionParameters callParameters) {
var message = callParameters.getMessage();
var fileContextIncluded =
hasReferencedFilePaths(message) || hasReferencedFilePaths(conversation);
return new ResponsePanel()
.withReloadAction(() -> reloadMessage(callParameters))
.withDeleteAction(() -> removeMessage(message.getId(), conversation))
.addContent(
new ChatMessageResponseBody(
project,
true,
false,
message.isWebSearchIncluded(),
fileContextIncluded || message.getDocumentationDetails() != null,
this));
var panel = new ResponseMessagePanel();
panel.addCopyAction(() -> CopyAction.copyToClipboard(message.getResponse()));
panel.addContent(new ChatMessageResponseBody(
project,
true,
false,
message.isWebSearchIncluded(),
fileContextIncluded || message.getDocumentationDetails() != null,
this));
return panel;
}
private void reloadMessage(ChatCompletionParameters prevParameters) {
private void reloadMessage(ChatCompletionParameters prevParameters,
UserMessagePanel userMessagePanel) {
var prevMessage = prevParameters.getMessage();
ResponsePanel responsePanel = null;
ResponseMessagePanel responsePanel = null;
try {
responsePanel = toolWindowScrollablePanel.getMessageResponsePanel(prevMessage.getId());
responsePanel = toolWindowScrollablePanel.getResponseMessagePanel(prevMessage.getId());
((ChatMessageResponseBody) responsePanel.getContent()).clear();
toolWindowScrollablePanel.update();
} catch (Exception e) {
@ -226,7 +239,7 @@ public class ChatToolWindowTabPanel implements Disposable {
if (responsePanel != null) {
prevMessage.setResponse("");
conversationService.saveMessage(conversation, prevMessage);
call(prevParameters.toBuilder().retry(true).build(), responsePanel);
call(prevParameters.toBuilder().retry(true).build(), responsePanel, userMessagePanel);
}
totalTokensPanel.updateConversationTokens(conversation);
@ -253,8 +266,11 @@ public class ChatToolWindowTabPanel implements Disposable {
totalTokensPanel.updateConversationTokens(conversation);
}
private void call(ChatCompletionParameters callParameters, ResponsePanel responsePanel) {
var responseContainer = (ChatMessageResponseBody) responsePanel.getContent();
private void call(
ChatCompletionParameters callParameters,
ResponseMessagePanel responseMessagePanel,
UserMessagePanel userMessagePanel) {
var responseContainer = (ChatMessageResponseBody) responseMessagePanel.getContent();
if (!CompletionRequestService.isRequestAllowed()) {
responseContainer.displayMissingCredential();
@ -264,15 +280,18 @@ public class ChatToolWindowTabPanel implements Disposable {
requestHandler = new ToolwindowChatCompletionRequestHandler(
new ToolWindowCompletionResponseEventListener(
conversationService,
responsePanel,
userMessagePanel,
responseMessagePanel,
totalTokensPanel,
userInputPanel) {
@Override
public void handleTokensExceededPolicyAccepted() {
call(callParameters, responsePanel);
call(callParameters, responseMessagePanel, userMessagePanel);
}
});
userInputPanel.setSubmitEnabled(false);
userMessagePanel.disableActions(List.of("RELOAD", "DELETE"));
responseMessagePanel.disableActions(List.of("COPY"));
requestHandler.call(callParameters);
}
@ -337,30 +356,41 @@ public class ChatToolWindowTabPanel implements Disposable {
private void displayConversation() {
clearWindow();
conversation.getMessages().forEach(message -> {
var response = message.getResponse() == null ? "" : message.getResponse();
var messageResponseBody =
new ChatMessageResponseBody(project, this).withResponse(response);
messageResponseBody.hideCaret();
var userMessagePanel = new UserMessagePanel(project, message, this);
var imageFilePath = message.getImageFilePath();
if (imageFilePath != null && !imageFilePath.isEmpty()) {
userMessagePanel.displayImage(imageFilePath);
}
var messagePanel = toolWindowScrollablePanel.addMessage(message.getId());
messagePanel.add(userMessagePanel);
messagePanel.add(new ResponsePanel()
.withReloadAction(() -> reloadMessage(
ChatCompletionParameters.builder(conversation, message)
.conversationType(ConversationType.DEFAULT)
.build()))
.withDeleteAction(() -> removeMessage(message.getId(), conversation))
.addContent(messageResponseBody));
messagePanel.add(getUserMessagePanel(message));
messagePanel.add(getResponseMessagePanel(message));
});
}
private UserMessagePanel getUserMessagePanel(Message message) {
var userMessagePanel = new UserMessagePanel(project, message, this);
userMessagePanel.addCopyAction(() -> CopyAction.copyToClipboard(message.getPrompt()));
userMessagePanel.addReloadAction(() -> reloadMessage(
ChatCompletionParameters.builder(conversation, message)
.conversationType(ConversationType.DEFAULT)
.build(),
userMessagePanel));
userMessagePanel.addDeleteAction(() -> removeMessage(message.getId(), conversation));
var imageFilePath = message.getImageFilePath();
if (imageFilePath != null && !imageFilePath.isEmpty()) {
userMessagePanel.displayImage(imageFilePath);
}
return userMessagePanel;
}
private ResponseMessagePanel getResponseMessagePanel(Message message) {
var response = message.getResponse() == null ? "" : message.getResponse();
var messageResponseBody =
new ChatMessageResponseBody(project, this).withResponse(response);
messageResponseBody.hideCaret();
var responseMessagePanel = new ResponseMessagePanel();
responseMessagePanel.addContent(messageResponseBody);
responseMessagePanel.addCopyAction(() -> CopyAction.copyToClipboard(message.getResponse()));
return responseMessagePanel;
}
private JPanel createRootPanel() {
var rootPanel = new JPanel(new BorderLayout());
rootPanel.add(createScrollPaneWithSmartScroller(toolWindowScrollablePanel),

View file

@ -37,9 +37,7 @@ public class ResponseNodeRenderer implements NodeRenderer {
}
private void renderHeading(Heading node, NodeRendererContext context, HtmlWriter html) {
if (node.getLevel() == 3) {
html.attr("style", "margin-top: 4px; margin-bottom: 4px;");
}
html.attr("style", "margin-top: 8px; margin-bottom: 4px;");
context.delegateRender();
}

View file

@ -13,8 +13,9 @@ import ee.carlrobert.codegpt.conversations.message.Message;
import ee.carlrobert.codegpt.events.CodeGPTEvent;
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.ui.ResponseMessagePanel;
import ee.carlrobert.codegpt.toolwindow.ui.UserMessagePanel;
import ee.carlrobert.codegpt.ui.OverlayUtil;
import ee.carlrobert.codegpt.ui.textarea.UserInputPanel;
import ee.carlrobert.llm.client.openai.completion.ErrorDetails;
@ -31,7 +32,8 @@ abstract class ToolWindowCompletionResponseEventListener implements
private final StringBuilder messageBuilder = new StringBuilder();
private final EncodingManager encodingManager;
private final ConversationService conversationService;
private final ResponsePanel responsePanel;
private final ResponseMessagePanel responsePanel;
private final UserMessagePanel userMessagePanel;
private final ChatMessageResponseBody responseContainer;
private final TotalTokensPanel totalTokensPanel;
private final UserInputPanel textArea;
@ -43,11 +45,13 @@ abstract class ToolWindowCompletionResponseEventListener implements
public ToolWindowCompletionResponseEventListener(
ConversationService conversationService,
ResponsePanel responsePanel,
UserMessagePanel userMessagePanel,
ResponseMessagePanel responsePanel,
TotalTokensPanel totalTokensPanel,
UserInputPanel textArea) {
this.encodingManager = EncodingManager.getInstance();
this.conversationService = conversationService;
this.userMessagePanel = userMessagePanel;
this.responsePanel = responsePanel;
this.responseContainer = (ChatMessageResponseBody) responsePanel.getContent();
this.totalTokensPanel = totalTokensPanel;
@ -89,7 +93,7 @@ abstract class ToolWindowCompletionResponseEventListener implements
}
} finally {
LOG.error(error.getMessage(), ex);
responsePanel.enableActions();
responsePanel.enableAllActions(true);
stopStreaming(responseContainer);
}
});
@ -119,7 +123,7 @@ abstract class ToolWindowCompletionResponseEventListener implements
ApplicationManager.getApplication().invokeLater(() -> {
try {
responsePanel.enableActions();
responsePanel.enableAllActions(true);
if (!streamResponseReceived && !fullMessage.isEmpty()) {
responseContainer.withResponse(fullMessage);
}
@ -156,6 +160,8 @@ abstract class ToolWindowCompletionResponseEventListener implements
private void stopStreaming(ChatMessageResponseBody responseContainer) {
stopped = true;
textArea.setSubmitEnabled(true);
userMessagePanel.enableAllActions(true);
responsePanel.enableAllActions(true);
responseContainer.hideCaret();
}
}

View file

@ -8,7 +8,6 @@ import ee.carlrobert.codegpt.actions.ActionType;
import ee.carlrobert.codegpt.actions.TrackableAction;
import ee.carlrobert.codegpt.ui.OverlayUtil;
import java.awt.Toolkit;
import java.awt.datatransfer.Clipboard;
import java.awt.datatransfer.StringSelection;
import java.awt.event.MouseEvent;
import org.jetbrains.annotations.NotNull;
@ -19,8 +18,8 @@ public class CopyAction extends TrackableAction {
public CopyAction(@NotNull Editor toolwindowEditor) {
super(
CodeGPTBundle.get("shared.copy"),
CodeGPTBundle.get("toolwindow.chat.editor.action.copy.description"),
CodeGPTBundle.get("shared.copyCode"),
CodeGPTBundle.get("shared.copyToClipboard"),
Actions.Copy,
ActionType.COPY_CODE);
this.toolwindowEditor = toolwindowEditor;
@ -28,17 +27,24 @@ public class CopyAction extends TrackableAction {
@Override
public void handleAction(@NotNull AnActionEvent event) {
StringSelection stringSelection = new StringSelection(toolwindowEditor.getDocument().getText());
Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard();
clipboard.setContents(stringSelection, null);
copyToClipboard(toolwindowEditor.getDocument().getText());
showCopyBalloon(event);
}
public static void copyToClipboard(String text) {
Toolkit.getDefaultToolkit()
.getSystemClipboard()
.setContents(new StringSelection(text), null);
}
public static void showCopyBalloon(AnActionEvent event) {
var mouseEvent = (MouseEvent) event.getInputEvent();
if (mouseEvent != null) {
var locationOnScreen = mouseEvent.getLocationOnScreen();
locationOnScreen.y = locationOnScreen.y - 16;
OverlayUtil.showInfoBalloon(
CodeGPTBundle.get("toolwindow.chat.editor.action.copy.success"),
CodeGPTBundle.get("shared.copiedToClipboard"),
locationOnScreen);
}
}

View file

@ -9,6 +9,7 @@ import com.intellij.icons.AllIcons;
import com.intellij.icons.AllIcons.General;
import com.intellij.openapi.Disposable;
import com.intellij.openapi.actionSystem.ActionPlaces;
import com.intellij.openapi.actionSystem.ActionUpdateThread;
import com.intellij.openapi.actionSystem.AnAction;
import com.intellij.openapi.actionSystem.AnActionEvent;
import com.intellij.openapi.actionSystem.DefaultActionGroup;
@ -37,12 +38,15 @@ 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.chat.editor.actions.CopyAction;
import ee.carlrobert.codegpt.toolwindow.ui.ResponseBodyProgressPanel;
import ee.carlrobert.codegpt.toolwindow.ui.WebpageList;
import ee.carlrobert.codegpt.ui.OverlayUtil;
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.event.MouseEvent;
import java.util.Objects;
import javax.swing.BoxLayout;
import javax.swing.DefaultListModel;
@ -331,9 +335,16 @@ public class ChatMessageResponseBody extends JPanel {
CodeGPTBundle.get("shared.copy"),
CodeGPTBundle.get("shared.copyToClipboard"),
AllIcons.Actions.Copy) {
@Override
public void actionPerformed(@NotNull AnActionEvent e) {
public @NotNull ActionUpdateThread getActionUpdateThread() {
return ActionUpdateThread.EDT;
}
@Override
public void actionPerformed(@NotNull AnActionEvent event) {
textPane.copy();
CopyAction.showCopyBalloon(event);
}
@Override

View file

@ -12,6 +12,7 @@ import ee.carlrobert.codegpt.credentials.CredentialsStore.CredentialKey;
import ee.carlrobert.codegpt.settings.GeneralSettings;
import ee.carlrobert.codegpt.settings.service.ServiceType;
import ee.carlrobert.codegpt.settings.service.codegpt.CodeGPTServiceConfigurable;
import ee.carlrobert.codegpt.toolwindow.ui.ResponseMessagePanel;
import ee.carlrobert.codegpt.ui.UIUtil;
import ee.carlrobert.codegpt.util.ApplicationUtil;
import java.util.Arrays;
@ -36,35 +37,35 @@ public class ChatToolWindowScrollablePanel extends ScrollablePanel {
if (GeneralSettings.isSelected(ServiceType.CODEGPT)
&& !CredentialsStore.INSTANCE.isCredentialSet(CredentialKey.CODEGPT_API_KEY)) {
var panel = new ResponsePanel()
.addContent(UIUtil.createTextPane("""
<html>
<p style="margin-top: 4px; margin-bottom: 4px;">
It looks like you haven't configured your API key yet. Visit <a href="#OPEN_SETTINGS">CodeGPT settings</a> to do so.
</p>
<p style="margin-top: 4px; margin-bottom: 4px;">
Don't have an account? <a href="https://codegpt.ee">Sign up</a> to get the most out of CodeGPT.
</p>
</html>""",
false,
event -> {
if (ACTIVATED.equals(event.getEventType())
&& "#OPEN_SETTINGS".equals(event.getDescription())) {
ShowSettingsUtil.getInstance().showSettingsDialog(
ApplicationUtil.findCurrentProject(),
CodeGPTServiceConfigurable.class);
} else {
UIUtil.handleHyperlinkClicked(event);
}
}));
var panel = new ResponseMessagePanel();
panel.addContent(UIUtil.createTextPane("""
<html>
<p style="margin-top: 4px; margin-bottom: 4px;">
It looks like you haven't configured your API key yet. Visit <a href="#OPEN_SETTINGS">CodeGPT settings</a> to do so.
</p>
<p style="margin-top: 4px; margin-bottom: 4px;">
Don't have an account? <a href="https://codegpt.ee">Sign up</a> to get the most out of CodeGPT.
</p>
</html>""",
false,
event -> {
if (ACTIVATED.equals(event.getEventType())
&& "#OPEN_SETTINGS".equals(event.getDescription())) {
ShowSettingsUtil.getInstance().showSettingsDialog(
ApplicationUtil.findCurrentProject(),
CodeGPTServiceConfigurable.class);
} else {
UIUtil.handleHyperlinkClicked(event);
}
}));
panel.setBorder(JBUI.Borders.customLine(JBColor.border(), 1, 0, 0, 0));
add(panel);
}
}
public ResponsePanel getMessageResponsePanel(UUID messageId) {
return (ResponsePanel) Arrays.stream(visibleMessagePanels.get(messageId).getComponents())
.filter(ResponsePanel.class::isInstance)
public ResponseMessagePanel getResponseMessagePanel(UUID messageId) {
return (ResponseMessagePanel) Arrays.stream(visibleMessagePanels.get(messageId).getComponents())
.filter(ResponseMessagePanel.class::isInstance)
.findFirst().orElseThrow();
}

View file

@ -1,150 +0,0 @@
package ee.carlrobert.codegpt.toolwindow.chat.ui;
import com.intellij.icons.AllIcons.Actions;
import com.intellij.openapi.actionSystem.AnAction;
import com.intellij.openapi.actionSystem.AnActionEvent;
import com.intellij.ui.components.JBLabel;
import com.intellij.util.ui.JBFont;
import com.intellij.util.ui.JBUI;
import ee.carlrobert.codegpt.CodeGPTBundle;
import ee.carlrobert.codegpt.Icons;
import ee.carlrobert.codegpt.ui.IconActionButton;
import java.awt.BorderLayout;
import java.awt.FlowLayout;
import javax.swing.Box;
import javax.swing.JComponent;
import javax.swing.JPanel;
import javax.swing.SwingConstants;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
public class ResponsePanel extends JPanel {
private final Header header;
private final Body body;
public ResponsePanel() {
super(new BorderLayout());
header = new Header();
body = new Body();
add(header, BorderLayout.NORTH);
add(body, BorderLayout.CENTER);
}
public void enableActions() {
header.enableActions(true);
}
public ResponsePanel withReloadAction(Runnable onReload) {
header.addReloadAction(onReload);
return this;
}
public ResponsePanel withDeleteAction(Runnable onDelete) {
header.addDeleteAction(onDelete);
return this;
}
public ResponsePanel addContent(JComponent content) {
body.addContent(content);
return this;
}
public void updateContent(JComponent content) {
body.updateContent(content);
}
public JComponent getContent() {
return body.getContent();
}
static class Header extends JPanel {
private final JPanel iconsWrapper;
Header() {
super(new BorderLayout());
setBorder(JBUI.Borders.empty(12, 8, 4, 8));
add(getIconLabel(), BorderLayout.LINE_START);
iconsWrapper = new JPanel(new FlowLayout(FlowLayout.RIGHT, 0, 0));
iconsWrapper.setBackground(getBackground());
add(iconsWrapper, BorderLayout.LINE_END);
}
public void enableActions(boolean enabled) {
for (var iconButton : iconsWrapper.getComponents()) {
iconButton.setEnabled(enabled);
}
}
public void addReloadAction(Runnable onReload) {
addIconActionButton(new IconActionButton(
new AnAction(
CodeGPTBundle.get("toolwindow.chat.response.action.reloadResponse.text"),
CodeGPTBundle.get("toolwindow.chat.response.action.reloadResponse.description"),
Actions.Refresh) {
@Override
public void actionPerformed(@NotNull AnActionEvent e) {
enableActions(false);
onReload.run();
}
}));
}
public void addDeleteAction(Runnable onDelete) {
addIconActionButton(new IconActionButton(
new AnAction(
CodeGPTBundle.get("toolwindow.chat.response.action.deleteResponse.text"),
CodeGPTBundle.get("toolwindow.chat.response.action.deleteResponse.description"),
Actions.GC) {
@Override
public void actionPerformed(@NotNull AnActionEvent e) {
onDelete.run();
}
}));
}
private void addIconActionButton(IconActionButton iconActionButton) {
if (iconsWrapper.getComponents() != null && iconsWrapper.getComponents().length > 0) {
iconsWrapper.add(Box.createHorizontalStrut(8));
}
iconsWrapper.add(iconActionButton);
}
private JBLabel getIconLabel() {
return new JBLabel(
CodeGPTBundle.get("project.label"),
Icons.Default,
SwingConstants.LEADING)
.setAllowAutoWrapping(true)
.withFont(JBFont.label().asBold());
}
}
static class Body extends JPanel {
private @Nullable JComponent content;
Body() {
super(new BorderLayout());
setBorder(JBUI.Borders.empty(4, 8));
}
public void addContent(JComponent content) {
this.content = content;
add(content);
}
public void updateContent(JComponent content) {
removeAll();
revalidate();
repaint();
addContent(content);
}
public @Nullable JComponent getContent() {
return content;
}
}
}

View file

@ -1,127 +0,0 @@
package ee.carlrobert.codegpt.toolwindow.chat.ui;
import com.intellij.icons.AllIcons.General;
import com.intellij.openapi.Disposable;
import com.intellij.openapi.project.Project;
import com.intellij.ui.ColorUtil;
import com.intellij.ui.JBColor;
import com.intellij.ui.components.JBLabel;
import com.intellij.util.ui.JBFont;
import com.intellij.util.ui.JBUI;
import ee.carlrobert.codegpt.CodeGPTBundle;
import ee.carlrobert.codegpt.CodeGPTKeys;
import ee.carlrobert.codegpt.Icons;
import ee.carlrobert.codegpt.conversations.message.Message;
import ee.carlrobert.codegpt.events.WebSearchEventDetails;
import ee.carlrobert.codegpt.settings.GeneralSettings;
import ee.carlrobert.codegpt.toolwindow.ui.WebpageList;
import java.awt.BorderLayout;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.UUID;
import javax.swing.DefaultListModel;
import javax.swing.JPanel;
import javax.swing.SwingConstants;
import org.jetbrains.annotations.Nullable;
public class UserMessagePanel extends JPanel {
public UserMessagePanel(Project project, Message message, Disposable parentDisposable) {
super(new BorderLayout());
var headerPanel = new JPanel(new BorderLayout());
headerPanel.setOpaque(false);
headerPanel.add(createDisplayNameLabel(), BorderLayout.LINE_START);
setBorder(JBUI.Borders.compound(
JBUI.Borders.customLine(JBColor.border(), 1, 0, 1, 0),
JBUI.Borders.empty(12, 8, 8, 8)));
setBackground(ColorUtil.brighter(getBackground(), 2));
add(headerPanel, BorderLayout.NORTH);
var additionalContextPanel = getAdditionalContextPanel(project, message);
if (additionalContextPanel != null) {
add(additionalContextPanel, BorderLayout.CENTER);
}
if (message.getImageFilePath() != null && !message.getImageFilePath().isEmpty()) {
displayImage(message.getImageFilePath());
}
add(createResponseBody(
project,
message.getPrompt(),
parentDisposable), BorderLayout.SOUTH);
}
public @Nullable JPanel getAdditionalContextPanel(Project project, Message message) {
var addedDocumentation = CodeGPTKeys.ADDED_DOCUMENTATION.get(project);
var referencedFilePaths = message.getReferencedFilePaths();
if (addedDocumentation == null
&& (referencedFilePaths == null
|| referencedFilePaths.isEmpty())) {
return null;
}
var panel = new JPanel(new BorderLayout());
panel.setOpaque(false);
if (addedDocumentation != null) {
var listModel = new DefaultListModel<WebSearchEventDetails>();
listModel.addElement(
new WebSearchEventDetails(UUID.randomUUID(), addedDocumentation.getName(),
addedDocumentation.getUrl(), addedDocumentation.getUrl()));
panel.add(createWebpageListPanel(new WebpageList(listModel)), BorderLayout.NORTH);
}
if (referencedFilePaths != null && !referencedFilePaths.isEmpty()) {
panel.add(new SelectedFilesAccordion(project, referencedFilePaths), BorderLayout.NORTH);
}
return panel;
}
public void displayImage(String imageFilePath) {
try {
var path = Paths.get(imageFilePath);
add(new ImageAccordion(path.getFileName().toString(), Files.readAllBytes(path)));
} catch (IOException e) {
add(new JBLabel(
"<html><small>Unable to load image %s</small></html>".formatted(imageFilePath),
General.Error,
SwingConstants.LEFT));
}
}
private ChatMessageResponseBody createResponseBody(
Project project,
String prompt,
Disposable parentDisposable) {
return new ChatMessageResponseBody(project, false, true, false, false, parentDisposable)
.withResponse(prompt);
}
private JBLabel createDisplayNameLabel() {
return new JBLabel(
GeneralSettings.getCurrentState().getDisplayName(),
Icons.User,
SwingConstants.LEADING)
.setAllowAutoWrapping(true)
.withFont(JBFont.label().asBold())
.withBorder(JBUI.Borders.emptyBottom(6));
}
private static JPanel createWebpageListPanel(WebpageList webpageList) {
var title = new JPanel(new BorderLayout());
title.setOpaque(false);
title.setBorder(JBUI.Borders.empty(8, 0));
title.add(new JBLabel(CodeGPTBundle.get("userMessagePanel.documentation.title"))
.withFont(JBUI.Fonts.miniFont()), BorderLayout.LINE_START);
var listPanel = new JPanel(new BorderLayout());
listPanel.setOpaque(false);
listPanel.add(webpageList, BorderLayout.LINE_START);
var panel = new JPanel(new BorderLayout());
panel.setOpaque(false);
panel.add(title, BorderLayout.NORTH);
panel.add(listPanel, BorderLayout.CENTER);
return panel;
}
}

View file

@ -87,7 +87,7 @@ class ConversationPanel extends JPanel {
gbc.gridx = 1;
gbc.weightx = 0;
headerPanel.add(new IconActionButton(new DeleteConversationAction(onDelete)), gbc);
headerPanel.add(new IconActionButton(new DeleteConversationAction(onDelete), "DELETE"), gbc);
var bottomPanel = new JPanel(new BorderLayout());
bottomPanel.add(new JLabel(conversation.getUpdatedOn()

View file

@ -8,11 +8,12 @@ import com.intellij.openapi.actionSystem.impl.ActionButton;
public class IconActionButton extends ActionButton {
public IconActionButton(AnAction action) {
public IconActionButton(AnAction action, String actionCode) {
super(action,
getPresentation(action),
ActionPlaces.TOOLWINDOW_CONTENT,
ActionToolbar.DEFAULT_MINIMUM_BUTTON_SIZE);
putClientProperty("actionCode", actionCode);
}
private static Presentation getPresentation(AnAction action) {