Add tabs for multiple concurrent conversations (#54)

This commit is contained in:
Carl-Robert Linnupuu 2023-03-22 23:42:32 +00:00
parent 638fb557a7
commit 3d104c8b5a
11 changed files with 158 additions and 94 deletions

View file

@ -3,9 +3,8 @@ package ee.carlrobert.codegpt.action;
import com.intellij.icons.AllIcons;
import com.intellij.openapi.actionSystem.AnAction;
import com.intellij.openapi.actionSystem.AnActionEvent;
import ee.carlrobert.codegpt.conversations.ConversationsState;
import ee.carlrobert.codegpt.toolwindow.ContentManagerService;
import ee.carlrobert.codegpt.toolwindow.ToolWindowService;
import ee.carlrobert.codegpt.toolwindow.chat.ChatToolWindowPanel;
import org.jetbrains.annotations.NotNull;
public class AskAction extends AnAction {
@ -23,10 +22,15 @@ public class AskAction extends AnAction {
public void actionPerformed(@NotNull AnActionEvent event) {
var project = event.getProject();
if (project != null) {
ConversationsState.getInstance().startConversation();
project.getService(ContentManagerService.class).displayChatTab(project);
var chatToolWindow = project.getService(ToolWindowService.class).getChatToolWindow();
chatToolWindow.displayLandingView();
var contentManagerService = project.getService(ContentManagerService.class);
contentManagerService.displayChatTab(project);
contentManagerService.tryFindChatTabbedPane(project)
.ifPresent(tabbedPane -> {
var panel = new ChatToolWindowPanel(project);
panel.displayLandingView();
tabbedPane.addTab("Chat " + (tabbedPane.getTabCount() + 1), panel);
tabbedPane.setSelectedIndex(tabbedPane.getTabCount() - 1);
});
}
}
}

View file

@ -6,9 +6,8 @@ import com.intellij.openapi.actionSystem.PlatformDataKeys;
import com.intellij.openapi.editor.Editor;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.NlsActions;
import ee.carlrobert.codegpt.conversations.ConversationsState;
import ee.carlrobert.codegpt.toolwindow.ContentManagerService;
import ee.carlrobert.codegpt.toolwindow.ToolWindowService;
import ee.carlrobert.codegpt.toolwindow.chat.ChatToolWindowPanel;
import javax.swing.Icon;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
@ -47,11 +46,15 @@ public abstract class BaseAction extends AnAction {
}
protected void sendMessage(@NotNull Project project, String prompt) {
ConversationsState.getInstance().startConversation();
project.getService(ContentManagerService.class).displayChatTab(project);
var chatToolWindow = project.getService(ToolWindowService.class).getChatToolWindow();
chatToolWindow.clearWindow();
chatToolWindow.displayUserMessage(prompt);
chatToolWindow.sendMessage(prompt, project);
var contentManagerService = project.getService(ContentManagerService.class);
contentManagerService.displayChatTab(project);
contentManagerService.tryFindChatTabbedPane(project)
.ifPresent(tabbedPane -> {
var panel = new ChatToolWindowPanel(project);
panel.displayUserMessage(prompt);
tabbedPane.addTab("Chat " + (tabbedPane.getTabCount() + 1), panel);
tabbedPane.setSelectedIndex(tabbedPane.getTabCount() - 1);
panel.sendMessage(prompt, project);
});
}
}

View file

@ -95,16 +95,16 @@ public class SettingsComponent {
return (TextCompletionModel) textCompletionBaseModelComboBox.getSelectedItem();
}
public void setTextCompletionBaseModel(String model) {
textCompletionBaseModelComboBox.setSelectedItem(TextCompletionModel.valueOf(model));
public void setTextCompletionBaseModel(String modelCode) {
textCompletionBaseModelComboBox.setSelectedItem(TextCompletionModel.findByCode(modelCode));
}
public ChatCompletionModel getChatCompletionBaseModel() {
return (ChatCompletionModel) chatCompletionBaseModelComboBox.getSelectedItem();
}
public void setChatCompletionBaseModel(String model) {
chatCompletionBaseModelComboBox.setSelectedItem(ChatCompletionModel.valueOf(model));
public void setChatCompletionBaseModel(String modelCode) {
chatCompletionBaseModelComboBox.setSelectedItem(ChatCompletionModel.findByCode(modelCode));
}
private JPanel createMainSelectionForm() {

View file

@ -6,6 +6,7 @@ import com.intellij.openapi.project.Project;
import com.intellij.openapi.wm.ToolWindowManager;
import com.intellij.ui.content.Content;
import com.intellij.ui.content.ContentManager;
import ee.carlrobert.codegpt.toolwindow.chat.ChatTabbedPane;
import java.util.Arrays;
import java.util.Optional;
import org.jetbrains.annotations.NotNull;
@ -33,4 +34,18 @@ public class ContentManagerService {
.filter(contentManager::isSelected)
.isPresent();
}
public Optional<ChatTabbedPane> tryFindChatTabbedPane(@NotNull Project project) {
var toolWindow = requireNonNull(ToolWindowManager.getInstance(project).getToolWindow("CodeGPT"));
var chatTabContent = tryFindChatTabContent(toolWindow.getContentManager());
if (chatTabContent.isPresent()) {
var tabbedPane = Arrays.stream(chatTabContent.get().getComponent().getComponents())
.filter(component -> component instanceof ChatTabbedPane)
.findFirst();
if (tabbedPane.isPresent()) {
return Optional.of((ChatTabbedPane) tabbedPane.get());
}
}
return Optional.empty();
}
}

View file

@ -1,16 +1,23 @@
package ee.carlrobert.codegpt.toolwindow;
import com.intellij.openapi.actionSystem.ActionManager;
import com.intellij.openapi.actionSystem.ActionToolbar;
import com.intellij.openapi.actionSystem.DefaultActionGroup;
import com.intellij.openapi.project.DumbAware;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.ui.SimpleToolWindowPanel;
import com.intellij.openapi.wm.ToolWindow;
import com.intellij.openapi.wm.ToolWindowFactory;
import com.intellij.ui.content.ContentManagerEvent;
import com.intellij.ui.content.ContentManagerListener;
import com.intellij.ui.jcef.JBCefBrowser;
import ee.carlrobert.codegpt.client.ClientFactory;
import ee.carlrobert.codegpt.account.AccountDetailsState;
import ee.carlrobert.codegpt.client.ClientFactory;
import ee.carlrobert.codegpt.conversations.ConversationsState;
import ee.carlrobert.codegpt.toolwindow.chat.ChatGptToolWindow;
import ee.carlrobert.codegpt.toolwindow.chat.ChatTabbedPane;
import ee.carlrobert.codegpt.toolwindow.chat.ChatToolWindowPanel;
import ee.carlrobert.codegpt.toolwindow.chat.actions.CreateNewConversationAction;
import ee.carlrobert.codegpt.toolwindow.chat.actions.OpenInEditorAction;
import ee.carlrobert.codegpt.toolwindow.chat.actions.UsageToolbarLabelAction;
import ee.carlrobert.codegpt.toolwindow.conversations.ConversationsToolWindow;
import javax.swing.JComponent;
import org.jetbrains.annotations.NotNull;
@ -18,15 +25,18 @@ import org.jetbrains.annotations.NotNull;
public class ProjectToolWindowFactory implements ToolWindowFactory, DumbAware {
public void createToolWindowContent(@NotNull Project project, @NotNull ToolWindow toolWindow) {
var chatToolWindow = new ChatGptToolWindow(project);
var chatToolWindow = new ChatToolWindowPanel(project);
var conversationsToolWindow = new ConversationsToolWindow(project);
var toolWindowService = project.getService(ToolWindowService.class);
toolWindowService.setChatToolWindow(chatToolWindow);
var toolWindowPanel = new SimpleToolWindowPanel(true);
var contentManagerService = project.getService(ContentManagerService.class);
addContent(toolWindow, chatToolWindow.getContent(), "Chat");
var chatTabbedPane = new ChatTabbedPane();
chatTabbedPane.addTab("Chat 1", chatToolWindow);
toolWindowPanel.setToolbar(createActionToolbar(project, chatTabbedPane, toolWindowPanel).getComponent());
toolWindowPanel.setContent(chatTabbedPane);
addContent(toolWindow, toolWindowPanel, "Chat");
addContent(toolWindow, conversationsToolWindow.getContent(), "Conversation History");
addContent(toolWindow, new JBCefBrowser("https://chat.openai.com/chat").getComponent(), "Browser");
toolWindow.addContentManagerListener(new ContentManagerListener() {
public void selectionChanged(@NotNull ContentManagerEvent event) {
var content = event.getContent();
@ -43,6 +53,7 @@ public class ProjectToolWindowFactory implements ToolWindowFactory, DumbAware {
}
});
var contentManagerService = project.getService(ContentManagerService.class);
if (contentManagerService.isChatTabSelected(toolWindow.getContentManager())) {
var conversation = ConversationsState.getCurrentConversation();
if (conversation == null) {
@ -58,4 +69,22 @@ public class ProjectToolWindowFactory implements ToolWindowFactory, DumbAware {
var content = contentManager.getFactory().createContent(panel, displayName, false);
contentManager.addContent(content);
}
private ActionToolbar createActionToolbar(Project project, ChatTabbedPane tabbedPane, SimpleToolWindowPanel toolWindowPanel) {
var actionGroup = new DefaultActionGroup("TOOLBAR_ACTION_GROUP", false);
actionGroup.add(new CreateNewConversationAction(() -> {
tabbedPane.addTab("Chat " + (tabbedPane.getTabCount() + 1), new ChatToolWindowPanel(project));
tabbedPane.setSelectedIndex(tabbedPane.getTabCount() - 1);
toolWindowPanel.repaint();
toolWindowPanel.revalidate();
}));
actionGroup.add(new OpenInEditorAction());
actionGroup.addSeparator();
actionGroup.add(new UsageToolbarLabelAction());
// TODO: Data usage not enabled in stream mode https://community.openai.com/t/usage-info-in-api-responses/18862/11
// actionGroup.add(new TokenToolbarLabelAction());
return ActionManager.getInstance().createActionToolbar("NAVIGATION_BAR_TOOLBAR", actionGroup, false);
}
}

View file

@ -1,40 +1,27 @@
package ee.carlrobert.codegpt.toolwindow;
import com.intellij.ide.ui.LafManager;
import com.intellij.ide.ui.LafManagerListener;
import com.intellij.openapi.project.Project;
import ee.carlrobert.codegpt.client.ClientFactory;
import ee.carlrobert.codegpt.client.ClientRequestFactory;
import ee.carlrobert.codegpt.client.EventListener;
import ee.carlrobert.codegpt.conversations.ConversationsState;
import ee.carlrobert.codegpt.conversations.Conversation;
import ee.carlrobert.codegpt.conversations.message.Message;
import ee.carlrobert.codegpt.settings.SettingsState;
import ee.carlrobert.codegpt.toolwindow.chat.ChatGptToolWindow;
import ee.carlrobert.codegpt.toolwindow.chat.ChatToolWindowPanel;
import ee.carlrobert.codegpt.toolwindow.components.SyntaxTextArea;
import java.util.List;
import javax.swing.SwingWorker;
import okhttp3.sse.EventSource;
import org.jetbrains.annotations.NotNull;
public class ToolWindowService implements LafManagerListener {
public class ToolWindowService {
private ChatGptToolWindow chatToolWindow;
@Override
public void lookAndFeelChanged(@NotNull LafManager source) {
chatToolWindow.changeStyle();
}
public void setChatToolWindow(ChatGptToolWindow chatToolWindow) {
this.chatToolWindow = chatToolWindow;
}
public ChatGptToolWindow getChatToolWindow() {
return chatToolWindow;
}
public void startRequest(String prompt, SyntaxTextArea textArea, Project project, boolean isRetry) {
var conversation = ConversationsState.getInstance().getOrStartNew();
public void startRequest(
String prompt,
SyntaxTextArea textArea,
Project project,
boolean isRetry,
ChatToolWindowPanel toolWindow,
Conversation conversation) {
var conversationMessage = new Message(prompt);
new SwingWorker<Void, String>() {
@ -42,7 +29,7 @@ public class ToolWindowService implements LafManagerListener {
var eventListener = new EventListener(
conversationMessage,
textArea::append,
() -> chatToolWindow.stopGenerating(prompt, textArea, project),
() -> toolWindow.stopGenerating(prompt, textArea, project),
isRetry) {
public void onMessage(String message) {
publish(message);
@ -59,7 +46,7 @@ public class ToolWindowService implements LafManagerListener {
call = ClientFactory.getTextCompletionClient().stream(
requestFactory.buildTextCompletionRequest(settings), eventListener);
}
chatToolWindow.displayGenerateButton(call::cancel);
toolWindow.displayGenerateButton(call::cancel);
return null;
}
@ -68,7 +55,7 @@ public class ToolWindowService implements LafManagerListener {
try {
textArea.append(text);
conversationMessage.setResponse(textArea.getText());
chatToolWindow.scrollToBottom();
toolWindow.scrollToBottom();
} catch (Exception e) {
textArea.append("Something went wrong. Please try again later.");
throw new RuntimeException(e);

View file

@ -0,0 +1,28 @@
package ee.carlrobert.codegpt.toolwindow.chat;
import com.intellij.ui.components.JBTabbedPane;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import java.util.UUID;
public class ChatTabbedPane extends JBTabbedPane {
private final Map<Integer, ChatToolWindowPanel> activeTabMapping = new HashMap<>();
public ChatTabbedPane() {
setTabComponentInsets(null);
}
public void addTab(String title, ChatToolWindowPanel toolWindowPanel) {
super.addTab(title, toolWindowPanel.getContent());
activeTabMapping.put(getTabCount() - 1, toolWindowPanel);
}
public Optional<Integer> tryFindActiveConversationIndex(UUID conversationId) {
return activeTabMapping.entrySet().stream()
.filter(entry -> conversationId.equals(entry.getValue().getConversationId()))
.findFirst()
.map(Map.Entry::getKey);
}
}

View file

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<form xmlns="http://www.intellij.com/uidesigner/form/" version="1" bind-to-class="ee.carlrobert.codegpt.toolwindow.chat.ChatGptToolWindow">
<form xmlns="http://www.intellij.com/uidesigner/form/" version="1" bind-to-class="ee.carlrobert.codegpt.toolwindow.chat.ChatToolWindowPanel">
<grid id="27dc6" binding="chatGptToolWindowContent" layout-manager="GridLayoutManager" row-count="3" column-count="3" same-size-horizontally="false" same-size-vertically="false" hgap="-1" vgap="-1">
<margin top="0" left="0" bottom="0" right="0"/>
<constraints>

View file

@ -6,13 +6,9 @@ import static ee.carlrobert.codegpt.util.SwingUtils.justifyLeft;
import static java.lang.String.format;
import com.intellij.icons.AllIcons;
import com.intellij.openapi.actionSystem.ActionManager;
import com.intellij.openapi.actionSystem.ActionToolbar;
import com.intellij.openapi.actionSystem.DefaultActionGroup;
import com.intellij.openapi.options.ShowSettingsUtil;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.roots.ui.componentsList.components.ScrollablePanel;
import com.intellij.openapi.ui.SimpleToolWindowPanel;
import com.intellij.ui.JBColor;
import com.intellij.ui.components.JBScrollPane;
import ee.carlrobert.codegpt.account.AccountDetailsState;
@ -21,9 +17,6 @@ import ee.carlrobert.codegpt.conversations.ConversationsState;
import ee.carlrobert.codegpt.settings.SettingsConfigurable;
import ee.carlrobert.codegpt.settings.SettingsState;
import ee.carlrobert.codegpt.toolwindow.ToolWindowService;
import ee.carlrobert.codegpt.toolwindow.chat.actions.CreateNewConversationAction;
import ee.carlrobert.codegpt.toolwindow.chat.actions.OpenInEditorAction;
import ee.carlrobert.codegpt.toolwindow.chat.actions.UsageToolbarLabelAction;
import ee.carlrobert.codegpt.toolwindow.components.GenerateButton;
import ee.carlrobert.codegpt.toolwindow.components.LandingView;
import ee.carlrobert.codegpt.toolwindow.components.ScrollPane;
@ -36,6 +29,7 @@ import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
import javax.swing.BorderFactory;
import javax.swing.Box;
import javax.swing.BoxLayout;
@ -49,7 +43,7 @@ import javax.swing.SwingUtilities;
import org.fife.ui.rsyntaxtextarea.SyntaxConstants;
import org.jetbrains.annotations.NotNull;
public class ChatGptToolWindow {
public class ChatToolWindowPanel {
private static final List<SyntaxTextArea> textAreas = new ArrayList<>();
private final Project project;
@ -60,33 +54,18 @@ public class ChatGptToolWindow {
private JScrollPane textAreaScrollPane;
private GenerateButton generateButton;
private boolean isLandingViewVisible;
private UUID conversationId;
public ChatGptToolWindow(@NotNull Project project) {
public ChatToolWindowPanel(@NotNull Project project) {
this.project = project;
}
public JPanel getContent() {
SimpleToolWindowPanel panel = new SimpleToolWindowPanel(true);
panel.setContent(chatGptToolWindowContent);
var actionGroup = new DefaultActionGroup("TOOLBAR_ACTION_GROUP", false);
actionGroup.add(new CreateNewConversationAction());
actionGroup.add(new OpenInEditorAction());
actionGroup.addSeparator();
actionGroup.add(new UsageToolbarLabelAction());
// TODO: Data usage not enabled in stream mode https://community.openai.com/t/usage-info-in-api-responses/18862/11
// actionGroup.add(new TokenToolbarLabelAction());
ActionToolbar actionToolbar = ActionManager.getInstance()
.createActionToolbar("NAVIGATION_BAR_TOOLBAR", actionGroup, false);
panel.setToolbar(actionToolbar.getComponent());
return panel;
return chatGptToolWindowContent;
}
public void displayUserMessage(String userMessage) {
addIconLabel(AllIcons.General.User, AccountDetailsState.getInstance().accountName);
scrollablePanel.add(createTextPane(userMessage));
scrollablePanel.revalidate();
scrollablePanel.repaint();
@ -113,6 +92,7 @@ public class ChatGptToolWindow {
}
public void displayConversation(Conversation conversation) {
setConversationId(conversation.getId());
clearWindow();
conversation.getMessages().forEach(message -> {
displayUserMessage(message.getPrompt());
@ -149,8 +129,11 @@ public class ChatGptToolWindow {
textArea = new SyntaxTextArea(true, true, SyntaxConstants.SYNTAX_STYLE_MARKDOWN);
addTextArea(textArea);
}
var conversation = ConversationsState.getInstance().getOrStartNew();
setConversationId(conversation.getId());
project.getService(ToolWindowService.class)
.startRequest(prompt, textArea, project, isRetry);
.startRequest(prompt, textArea, project, isRetry, this, conversation);
}
}
@ -247,4 +230,12 @@ public class ChatGptToolWindow {
generateButton = new GenerateButton();
}
public UUID getConversationId() {
return conversationId;
}
public void setConversationId(UUID conversationId) {
this.conversationId = conversationId;
}
}

View file

@ -4,13 +4,15 @@ import com.intellij.icons.AllIcons;
import com.intellij.openapi.actionSystem.AnAction;
import com.intellij.openapi.actionSystem.AnActionEvent;
import ee.carlrobert.codegpt.conversations.ConversationsState;
import ee.carlrobert.codegpt.toolwindow.ToolWindowService;
import org.jetbrains.annotations.NotNull;
public class CreateNewConversationAction extends AnAction {
public CreateNewConversationAction() {
super("Create New Conversation", "Create new conversation", AllIcons.General.Add);
private final Runnable onCreate;
public CreateNewConversationAction(Runnable onCreate) {
super("Create New Chat", "Create new chat", AllIcons.General.Add);
this.onCreate = onCreate;
}
@Override
@ -18,9 +20,7 @@ public class CreateNewConversationAction extends AnAction {
var project = event.getProject();
if (project != null) {
ConversationsState.getInstance().startConversation();
project.getService(ToolWindowService.class)
.getChatToolWindow()
.displayLandingView();
onCreate.run();
}
}
}

View file

@ -13,7 +13,7 @@ import ee.carlrobert.codegpt.conversations.Conversation;
import ee.carlrobert.codegpt.conversations.ConversationsState;
import ee.carlrobert.codegpt.settings.SettingsState;
import ee.carlrobert.codegpt.toolwindow.ContentManagerService;
import ee.carlrobert.codegpt.toolwindow.ToolWindowService;
import ee.carlrobert.codegpt.toolwindow.chat.ChatToolWindowPanel;
import ee.carlrobert.codegpt.toolwindow.conversations.actions.ClearAllConversationsAction;
import ee.carlrobert.codegpt.toolwindow.conversations.actions.DeleteConversationAction;
import ee.carlrobert.codegpt.toolwindow.conversations.actions.MoveDownAction;
@ -76,17 +76,24 @@ public class ConversationsToolWindow {
var mainPanel = new RootConversationPanel(() -> {
ConversationsState.getInstance().setCurrentConversation(conversation);
changeSettings(conversation);
project.getService(ContentManagerService.class).displayChatTab(project);
project.getService(ToolWindowService.class)
.getChatToolWindow()
.displayConversation(conversation);
var contentManagerService = project.getService(ContentManagerService.class);
contentManagerService.displayChatTab(project);
contentManagerService.tryFindChatTabbedPane(project)
.ifPresent(tabbedPane -> tabbedPane.tryFindActiveConversationIndex(conversation.getId())
.ifPresentOrElse(tabbedPane::setSelectedIndex, () -> {
var panel = new ChatToolWindowPanel(project);
panel.displayConversation(conversation);
tabbedPane.addTab("Chat " + (tabbedPane.getTabCount() + 1), panel);
tabbedPane.setSelectedIndex(tabbedPane.getTabCount() - 1);
}));
});
mainPanel.setBackground(conversationsToolWindowContent.getBackground());
var currentConversation = ConversationsState.getCurrentConversation();
var isSelected = currentConversation != null && currentConversation.getId().equals(conversation.getId());
mainPanel.add(new ConversationPanel(conversation, isSelected));
mainPanel.setBackground(conversationsToolWindowContent.getBackground());
scrollablePanel.add(mainPanel);
}