Improve multiple concurrent conversations (#54)

This commit is contained in:
Carl-Robert Linnupuu 2023-03-23 22:38:03 +00:00
parent 3e00703412
commit b9abdbf0b0
17 changed files with 398 additions and 336 deletions

View file

@ -4,7 +4,7 @@ import com.intellij.openapi.project.Project;
import com.intellij.openapi.startup.StartupActivity;
import ee.carlrobert.codegpt.account.AccountDetailsState;
import ee.carlrobert.codegpt.action.ActionsUtil;
import ee.carlrobert.codegpt.client.ClientFactory;
import ee.carlrobert.codegpt.client.ClientProvider;
import ee.carlrobert.codegpt.settings.configuration.ConfigurationState;
import org.jetbrains.annotations.NotNull;
@ -15,7 +15,7 @@ public class PluginStartupActivity implements StartupActivity {
ActionsUtil.refreshActions(ConfigurationState.getInstance().tableData);
var accountDetails = AccountDetailsState.getInstance();
if ("User".equals(accountDetails.accountName) || accountDetails.accountName == null) {
ClientFactory.getBillingClient()
ClientProvider.getBillingClient()
.getSubscriptionAsync(subscription ->
accountDetails.accountName = subscription.getAccountName());
}

View file

@ -3,8 +3,9 @@ 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.toolwindow.ContentManagerService;
import ee.carlrobert.codegpt.toolwindow.chat.ChatToolWindowPanel;
import ee.carlrobert.codegpt.conversations.ConversationsState;
import ee.carlrobert.codegpt.toolwindow.chat.ChatContentManagerService;
import ee.carlrobert.codegpt.toolwindow.chat.ChatToolWindowTabPanel;
import org.jetbrains.annotations.NotNull;
public class AskAction extends AnAction {
@ -22,14 +23,14 @@ public class AskAction extends AnAction {
public void actionPerformed(@NotNull AnActionEvent event) {
var project = event.getProject();
if (project != null) {
var contentManagerService = project.getService(ContentManagerService.class);
var contentManagerService = project.getService(ChatContentManagerService.class);
contentManagerService.displayChatTab(project);
contentManagerService.tryFindChatTabbedPane(project)
.ifPresent(tabbedPane -> {
var panel = new ChatToolWindowPanel(project);
ConversationsState.getInstance().setCurrentConversation(null);
var panel = new ChatToolWindowTabPanel(project);
panel.displayLandingView();
tabbedPane.addTab("Chat " + (tabbedPane.getTabCount() + 1), panel);
tabbedPane.setSelectedIndex(tabbedPane.getTabCount() - 1);
tabbedPane.addNewTab(panel);
});
}
}

View file

@ -6,8 +6,9 @@ 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.toolwindow.ContentManagerService;
import ee.carlrobert.codegpt.toolwindow.chat.ChatToolWindowPanel;
import ee.carlrobert.codegpt.conversations.ConversationsState;
import ee.carlrobert.codegpt.toolwindow.chat.ChatContentManagerService;
import ee.carlrobert.codegpt.toolwindow.chat.ChatToolWindowTabPanel;
import javax.swing.Icon;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
@ -46,14 +47,15 @@ public abstract class BaseAction extends AnAction {
}
protected void sendMessage(@NotNull Project project, String prompt) {
var contentManagerService = project.getService(ContentManagerService.class);
var contentManagerService = project.getService(ChatContentManagerService.class);
contentManagerService.displayChatTab(project);
contentManagerService.tryFindChatTabbedPane(project)
.ifPresent(tabbedPane -> {
var panel = new ChatToolWindowPanel(project);
var conversation = ConversationsState.getInstance().startConversation();
var panel = new ChatToolWindowTabPanel(project);
panel.setConversationId(conversation.getId());
panel.displayUserMessage(prompt);
tabbedPane.addTab("Chat " + (tabbedPane.getTabCount() + 1), panel);
tabbedPane.setSelectedIndex(tabbedPane.getTabCount() - 1);
tabbedPane.addNewTab(panel);
panel.sendMessage(prompt, project);
});
}

View file

@ -11,7 +11,7 @@ import java.net.InetSocketAddress;
import java.net.Proxy;
import java.util.concurrent.TimeUnit;
public class ClientFactory {
public class ClientProvider {
public static BillingClient getBillingClient() {
return getClientBuilder().buildBillingClient();

View file

@ -1,7 +1,6 @@
package ee.carlrobert.codegpt.client;
import ee.carlrobert.codegpt.conversations.Conversation;
import ee.carlrobert.codegpt.settings.SettingsState;
import ee.carlrobert.openai.client.completion.chat.request.ChatCompletionMessage;
import ee.carlrobert.openai.client.completion.chat.request.ChatCompletionRequest;
import ee.carlrobert.openai.client.completion.text.TextCompletionModel;
@ -9,30 +8,30 @@ import ee.carlrobert.openai.client.completion.text.request.TextCompletionRequest
import java.util.ArrayList;
import java.util.List;
public class ClientRequestFactory {
public class CompletionRequestProvider {
private final String prompt;
private final Conversation conversation;
public ClientRequestFactory(String prompt, Conversation conversation) {
public CompletionRequestProvider(String prompt, Conversation conversation) {
this.prompt = prompt;
this.conversation = conversation;
}
public ChatCompletionRequest buildChatCompletionRequest(SettingsState settings) {
return new ChatCompletionRequest.Builder(buildMessages(prompt, conversation))
.setModel(settings.chatCompletionBaseModel)
public ChatCompletionRequest buildChatCompletionRequest(String model) {
return new ChatCompletionRequest.Builder(buildMessages())
.setModel(model)
.build();
}
public TextCompletionRequest buildTextCompletionRequest(SettingsState settings) {
return new TextCompletionRequest.Builder(buildPrompt(conversation, prompt))
public TextCompletionRequest buildTextCompletionRequest(String model) {
return new TextCompletionRequest.Builder(buildPrompt(model))
.setStop(List.of(" Human:", " AI:"))
.setModel(settings.textCompletionBaseModel)
.setModel(model)
.build();
}
private List<ChatCompletionMessage> buildMessages(String prompt, Conversation conversation) {
private List<ChatCompletionMessage> buildMessages() {
var messages = new ArrayList<ChatCompletionMessage>();
messages.add(new ChatCompletionMessage(
"system",
@ -45,8 +44,8 @@ public class ClientRequestFactory {
return messages;
}
private StringBuilder getBasePrompt() {
var isDavinciModel = TextCompletionModel.DAVINCI.getCode().equals(SettingsState.getInstance().textCompletionBaseModel);
private StringBuilder getBasePrompt(String model) {
var isDavinciModel = TextCompletionModel.DAVINCI.getCode().equals(model);
if (isDavinciModel) {
return new StringBuilder(
"You are ChatGPT, a large language model trained by OpenAI.\n" +
@ -56,8 +55,8 @@ public class ClientRequestFactory {
"The following is a conversation with an AI assistant. The assistant is helpful, creative, clever, and very friendly.\n\n");
}
private String buildPrompt(Conversation conversation, String prompt) {
var basePrompt = getBasePrompt();
private String buildPrompt(String model) {
var basePrompt = getBasePrompt(model);
conversation.getMessages().forEach(message ->
basePrompt.append("Human: ")
.append(message.getPrompt())

View file

@ -12,6 +12,7 @@ import ee.carlrobert.codegpt.settings.SettingsState;
import ee.carlrobert.openai.client.ClientCode;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Comparator;
import java.util.List;
import java.util.Optional;
@ -163,4 +164,13 @@ public class ConversationsState implements PersistentStateComponent<Conversation
public Conversation getOrStartNew() {
return currentConversation == null ? startConversation() : currentConversation;
}
public Optional<Conversation> getConversation(UUID conversationId) {
return conversationsContainer.getConversationsMapping()
.values()
.stream()
.flatMap(Collection::stream)
.filter(it -> conversationId.equals(it.getId()))
.findFirst();
}
}

View file

@ -1,23 +1,14 @@
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 ee.carlrobert.codegpt.account.AccountDetailsState;
import ee.carlrobert.codegpt.client.ClientFactory;
import ee.carlrobert.codegpt.conversations.ConversationsState;
import ee.carlrobert.codegpt.toolwindow.chat.ChatTabbedPane;
import ee.carlrobert.codegpt.client.ClientProvider;
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;
@ -25,17 +16,10 @@ import org.jetbrains.annotations.NotNull;
public class ProjectToolWindowFactory implements ToolWindowFactory, DumbAware {
public void createToolWindowContent(@NotNull Project project, @NotNull ToolWindow toolWindow) {
var chatToolWindow = new ChatToolWindowPanel(project);
var conversationsToolWindow = new ConversationsToolWindow(project);
var toolWindowPanel = new SimpleToolWindowPanel(true);
var chatToolWindowPanel = new ChatToolWindowPanel(project, toolWindow);
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, chatToolWindowPanel, "Chat");
addContent(toolWindow, conversationsToolWindow.getContent(), "Conversation History");
toolWindow.addContentManagerListener(new ContentManagerListener() {
public void selectionChanged(@NotNull ContentManagerEvent event) {
@ -43,7 +27,7 @@ public class ProjectToolWindowFactory implements ToolWindowFactory, DumbAware {
if ("Conversation History".equals(content.getTabName()) && content.isSelected()) {
conversationsToolWindow.refresh();
} else if ("Chat".equals(content.getTabName()) && content.isSelected()) {
ClientFactory.getBillingClient()
ClientProvider.getBillingClient()
.getCreditUsageAsync(creditUsage -> {
var accountDetails = AccountDetailsState.getInstance();
accountDetails.totalAmountGranted = creditUsage.getTotalGranted();
@ -52,16 +36,6 @@ public class ProjectToolWindowFactory implements ToolWindowFactory, DumbAware {
}
}
});
var contentManagerService = project.getService(ContentManagerService.class);
if (contentManagerService.isChatTabSelected(toolWindow.getContentManager())) {
var conversation = ConversationsState.getCurrentConversation();
if (conversation == null) {
chatToolWindow.displayLandingView();
} else {
chatToolWindow.displayConversation(conversation);
}
}
}
public void addContent(ToolWindow toolWindow, JComponent panel, String displayName) {
@ -69,22 +43,4 @@ 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,13 +1,13 @@
package ee.carlrobert.codegpt.toolwindow;
import com.intellij.openapi.project.Project;
import ee.carlrobert.codegpt.client.ClientFactory;
import ee.carlrobert.codegpt.client.ClientRequestFactory;
import ee.carlrobert.codegpt.client.ClientProvider;
import ee.carlrobert.codegpt.client.CompletionRequestProvider;
import ee.carlrobert.codegpt.client.EventListener;
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.ChatToolWindowPanel;
import ee.carlrobert.codegpt.toolwindow.chat.ChatToolWindowTabPanel;
import ee.carlrobert.codegpt.toolwindow.components.SyntaxTextArea;
import java.util.List;
import javax.swing.SwingWorker;
@ -20,7 +20,7 @@ public class ToolWindowService {
SyntaxTextArea textArea,
Project project,
boolean isRetry,
ChatToolWindowPanel toolWindow,
ChatToolWindowTabPanel toolWindow,
Conversation conversation) {
var conversationMessage = new Message(prompt);
@ -38,13 +38,13 @@ public class ToolWindowService {
EventSource call;
var settings = SettingsState.getInstance();
var requestFactory = new ClientRequestFactory(prompt, conversation);
var requestProvider = new CompletionRequestProvider(prompt, conversation);
if (settings.isChatCompletionOptionSelected) {
call = ClientFactory.getChatCompletionClient().stream(
requestFactory.buildChatCompletionRequest(settings), eventListener);
call = ClientProvider.getChatCompletionClient().stream(
requestProvider.buildChatCompletionRequest(settings.chatCompletionBaseModel), eventListener);
} else {
call = ClientFactory.getTextCompletionClient().stream(
requestFactory.buildTextCompletionRequest(settings), eventListener);
call = ClientProvider.getTextCompletionClient().stream(
requestProvider.buildTextCompletionRequest(settings.textCompletionBaseModel), eventListener);
}
toolWindow.displayGenerateButton(call::cancel);
return null;

View file

@ -1,20 +1,20 @@
package ee.carlrobert.codegpt.toolwindow;
package ee.carlrobert.codegpt.toolwindow.chat;
import static java.util.Objects.requireNonNull;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.wm.ToolWindow;
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;
public class ContentManagerService {
public class ChatContentManagerService {
public void displayChatTab(@NotNull Project project) {
var toolWindow = requireNonNull(ToolWindowManager.getInstance(project).getToolWindow("CodeGPT"));
var toolWindow = getToolWindow(project);
toolWindow.show();
var contentManager = toolWindow.getContentManager();
tryFindChatTabContent(contentManager).ifPresentOrElse(
@ -23,12 +23,6 @@ public class ContentManagerService {
);
}
public Optional<Content> tryFindChatTabContent(ContentManager contentManager) {
return Arrays.stream(contentManager.getContents())
.filter(content -> "Chat".equals(content.getTabName()))
.findFirst();
}
public boolean isChatTabSelected(ContentManager contentManager) {
return tryFindChatTabContent(contentManager)
.filter(contentManager::isSelected)
@ -36,8 +30,7 @@ public class ContentManagerService {
}
public Optional<ChatTabbedPane> tryFindChatTabbedPane(@NotNull Project project) {
var toolWindow = requireNonNull(ToolWindowManager.getInstance(project).getToolWindow("CodeGPT"));
var chatTabContent = tryFindChatTabContent(toolWindow.getContentManager());
var chatTabContent = tryFindChatTabContent(getToolWindow(project).getContentManager());
if (chatTabContent.isPresent()) {
var tabbedPane = Arrays.stream(chatTabContent.get().getComponent().getComponents())
.filter(component -> component instanceof ChatTabbedPane)
@ -48,4 +41,23 @@ public class ContentManagerService {
}
return Optional.empty();
}
public void resetTabbedPane(@NotNull Project project) {
tryFindChatTabbedPane(project).ifPresent(tabbedPane -> {
tabbedPane.removeAll();
var tabPanel = new ChatToolWindowTabPanel(project);
tabPanel.displayLandingView();
tabbedPane.addNewTab(tabPanel);
});
}
private Optional<Content> tryFindChatTabContent(ContentManager contentManager) {
return Arrays.stream(contentManager.getContents())
.filter(content -> "Chat".equals(content.getTabName()))
.findFirst();
}
private ToolWindow getToolWindow(@NotNull Project project) {
return requireNonNull(ToolWindowManager.getInstance(project).getToolWindow("CodeGPT"));
}
}

View file

@ -1,6 +1,8 @@
package ee.carlrobert.codegpt.toolwindow.chat;
import com.intellij.ui.components.JBTabbedPane;
import ee.carlrobert.codegpt.conversations.Conversation;
import ee.carlrobert.codegpt.conversations.ConversationsState;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
@ -8,15 +10,19 @@ import java.util.UUID;
public class ChatTabbedPane extends JBTabbedPane {
private final Map<Integer, ChatToolWindowPanel> activeTabMapping = new HashMap<>();
private final Map<Integer, ChatToolWindowTabPanel> activeTabMapping = new HashMap<>();
public ChatTabbedPane() {
setTabComponentInsets(null);
addChangeListener(e -> tryFindCurrentlyActiveConversation()
.ifPresent(conversation -> ConversationsState.getInstance().setCurrentConversation(conversation)));
}
public void addTab(String title, ChatToolWindowPanel toolWindowPanel) {
super.addTab(title, toolWindowPanel.getContent());
activeTabMapping.put(getTabCount() - 1, toolWindowPanel);
public void addNewTab(ChatToolWindowTabPanel toolWindowPanel) {
super.addTab("Chat " + (getTabCount() + 1), toolWindowPanel.getContent());
var tabIndex = getTabCount() - 1;
super.setSelectedIndex(tabIndex);
activeTabMapping.put(tabIndex, toolWindowPanel);
}
public Optional<Integer> tryFindActiveConversationIndex(UUID conversationId) {
@ -25,4 +31,12 @@ public class ChatTabbedPane extends JBTabbedPane {
.findFirst()
.map(Map.Entry::getKey);
}
public Optional<Conversation> tryFindCurrentlyActiveConversation() {
var toolWindowPanel = activeTabMapping.get(getSelectedIndex());
if (toolWindowPanel == null || toolWindowPanel.getConversationId() == null) {
return Optional.empty();
}
return ConversationsState.getInstance().getConversation(toolWindowPanel.getConversationId());
}
}

View file

@ -1,241 +1,65 @@
package ee.carlrobert.codegpt.toolwindow.chat;
import static ee.carlrobert.codegpt.util.SwingUtils.createIconLabel;
import static ee.carlrobert.codegpt.util.SwingUtils.createTextPane;
import static ee.carlrobert.codegpt.util.SwingUtils.justifyLeft;
import static java.lang.String.format;
import com.intellij.icons.AllIcons;
import com.intellij.openapi.options.ShowSettingsUtil;
import com.intellij.openapi.actionSystem.ActionManager;
import com.intellij.openapi.actionSystem.ActionToolbar;
import com.intellij.openapi.actionSystem.DefaultActionGroup;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.roots.ui.componentsList.components.ScrollablePanel;
import com.intellij.ui.JBColor;
import com.intellij.ui.components.JBScrollPane;
import ee.carlrobert.codegpt.account.AccountDetailsState;
import ee.carlrobert.codegpt.conversations.Conversation;
import com.intellij.openapi.ui.SimpleToolWindowPanel;
import com.intellij.openapi.wm.ToolWindow;
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.components.GenerateButton;
import ee.carlrobert.codegpt.toolwindow.components.LandingView;
import ee.carlrobert.codegpt.toolwindow.components.ScrollPane;
import ee.carlrobert.codegpt.toolwindow.components.SyntaxTextArea;
import ee.carlrobert.codegpt.toolwindow.components.TextArea;
import icons.Icons;
import java.awt.Cursor;
import java.awt.Dimension;
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;
import javax.swing.Icon;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JScrollBar;
import javax.swing.JScrollPane;
import javax.swing.JTextArea;
import javax.swing.SwingUtilities;
import org.fife.ui.rsyntaxtextarea.SyntaxConstants;
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 org.jetbrains.annotations.NotNull;
public class ChatToolWindowPanel {
public class ChatToolWindowPanel extends SimpleToolWindowPanel {
private static final List<SyntaxTextArea> textAreas = new ArrayList<>();
private final Project project;
private JPanel chatGptToolWindowContent;
private ScrollPane scrollPane;
private ScrollablePanel scrollablePanel;
private JTextArea textArea;
private JScrollPane textAreaScrollPane;
private GenerateButton generateButton;
private boolean isLandingViewVisible;
private UUID conversationId;
public ChatToolWindowPanel(@NotNull Project project) {
this.project = project;
public ChatToolWindowPanel(@NotNull Project project, @NotNull ToolWindow toolWindow) {
super(true);
initialize(project, toolWindow);
}
public JPanel getContent() {
return chatGptToolWindowContent;
}
private void initialize(Project project, ToolWindow toolWindow) {
var tabPanel = new ChatToolWindowTabPanel(project);
var tabbedPane = createTabbedPane(tabPanel);
setToolbar(createActionToolbar(project, tabbedPane).getComponent());
setContent(tabbedPane);
public void displayUserMessage(String userMessage) {
addIconLabel(AllIcons.General.User, AccountDetailsState.getInstance().accountName);
scrollablePanel.add(createTextPane(userMessage));
scrollablePanel.revalidate();
scrollablePanel.repaint();
}
public void displayLandingView() {
if (!isLandingViewVisible) {
SwingUtilities.invokeLater(() -> {
clearWindow();
isLandingViewVisible = true;
addSpacing(16);
var landingView = new LandingView();
scrollablePanel.add(landingView.createImageIconPanel());
addSpacing(16);
landingView.getQuestionPanels().forEach(panel -> {
scrollablePanel.add(panel);
addSpacing(16);
});
scrollablePanel.revalidate();
scrollablePanel.repaint();
});
}
}
public void displayConversation(Conversation conversation) {
setConversationId(conversation.getId());
clearWindow();
conversation.getMessages().forEach(message -> {
displayUserMessage(message.getPrompt());
addIconLabel(Icons.DefaultImageIcon, "ChatGPT");
var textArea = new SyntaxTextArea(true, false, SyntaxConstants.SYNTAX_STYLE_MARKDOWN);
textArea.setText(message.getResponse());
textArea.displayCopyButton();
scrollablePanel.add(textArea);
textAreas.add(textArea);
});
scrollToBottom();
scrollablePanel.revalidate();
scrollablePanel.repaint();
}
public void sendMessage(String prompt, Project project) {
sendMessage(prompt, project, false);
}
public void sendMessage(String prompt, Project project, boolean isRetry) {
if (!isRetry) {
addIconLabel(Icons.DefaultImageIcon, "ChatGPT");
}
var settings = SettingsState.getInstance();
if (settings.apiKey.isEmpty()) {
notifyMissingCredential(project, "API key not provided.");
} else {
SyntaxTextArea textArea;
if (isRetry) {
textArea = textAreas.get(textAreas.size() - 1);
textArea.clear();
var contentManagerService = project.getService(ChatContentManagerService.class);
if (contentManagerService.isChatTabSelected(toolWindow.getContentManager())) {
var conversation = ConversationsState.getCurrentConversation();
if (conversation == null) {
tabPanel.displayLandingView();
} else {
textArea = new SyntaxTextArea(true, true, SyntaxConstants.SYNTAX_STYLE_MARKDOWN);
addTextArea(textArea);
tabPanel.displayConversation(conversation);
}
var conversation = ConversationsState.getInstance().getOrStartNew();
setConversationId(conversation.getId());
project.getService(ToolWindowService.class)
.startRequest(prompt, textArea, project, isRetry, this, conversation);
}
}
public void clearWindow() {
isLandingViewVisible = false;
generateButton.setVisible(false);
textAreas.clear();
scrollablePanel.removeAll();
private ActionToolbar createActionToolbar(Project project, ChatTabbedPane tabbedPane) {
var actionGroup = new DefaultActionGroup("TOOLBAR_ACTION_GROUP", false);
actionGroup.add(new CreateNewConversationAction(() -> {
var conversation = ConversationsState.getInstance().startConversation();
var panel = new ChatToolWindowTabPanel(project);
panel.setConversationId(conversation.getId());
panel.displayLandingView();
tabbedPane.addNewTab(panel);
repaint();
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);
}
public void addSpacing(int height) {
scrollablePanel.add(Box.createVerticalStrut(height));
}
public void addIconLabel(Icon icon, String text) {
addSpacing(8);
scrollablePanel.add(justifyLeft(createIconLabel(icon, text)));
addSpacing(8);
}
public void notifyMissingCredential(Project project, String text) {
var label = new JLabel(format("<html>%s <font color='#589df6'><u>Open Settings</u></font> to set one.</html>", text));
label.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR));
label.addMouseListener(new MouseAdapter() {
public void mouseClicked(MouseEvent e) {
ShowSettingsUtil.getInstance().showSettingsDialog(project, SettingsConfigurable.class);
}
});
scrollablePanel.add(justifyLeft(label));
}
public void displayGenerateButton(Runnable onClick) {
generateButton.setVisible(true);
generateButton.setMode(GenerateButton.Mode.STOP, onClick);
}
public void stopGenerating(String prompt, SyntaxTextArea textArea, Project project) {
generateButton.setMode(GenerateButton.Mode.REFRESH, () -> {
sendMessage(prompt, project, true);
scrollToBottom();
});
textArea.displayCopyButton();
textArea.getCaret().setVisible(false);
scrollToBottom();
}
public void scrollToBottom() {
scrollPane.scrollToBottom();
}
public void addTextArea(SyntaxTextArea textArea) {
scrollablePanel.add(textArea);
textAreas.add(textArea);
}
public void changeStyle() {
for (var textArea : textAreas) {
textArea.changeStyleViaThemeXml();
}
}
private void handleSubmit() {
var searchText = textArea.getText();
if (isLandingViewVisible || ConversationsState.getCurrentConversation() == null) {
clearWindow();
}
displayUserMessage(searchText);
sendMessage(searchText, project);
textArea.setText("");
scrollToBottom();
scrollablePanel.revalidate();
scrollablePanel.repaint();
}
private void createUIComponents() {
textAreaScrollPane = new JBScrollPane() {
public JScrollBar createVerticalScrollBar() {
JScrollBar verticalScrollBar = new JScrollPane.ScrollBar(1);
verticalScrollBar.setPreferredSize(new Dimension(0, 0));
return verticalScrollBar;
}
};
textAreaScrollPane.setBorder(null);
textAreaScrollPane.setViewportBorder(null);
textAreaScrollPane.setBorder(BorderFactory.createCompoundBorder(
BorderFactory.createMatteBorder(1, 0, 0, 0, JBColor.border()),
BorderFactory.createEmptyBorder(0, 5, 0, 10)));
textArea = new TextArea(this::handleSubmit, textAreaScrollPane);
textAreaScrollPane.setViewportView(textArea);
scrollablePanel = new ScrollablePanel();
scrollablePanel.setLayout(new BoxLayout(scrollablePanel, BoxLayout.Y_AXIS));
scrollPane = new ScrollPane(scrollablePanel);
generateButton = new GenerateButton();
}
public UUID getConversationId() {
return conversationId;
}
public void setConversationId(UUID conversationId) {
this.conversationId = conversationId;
private ChatTabbedPane createTabbedPane(ChatToolWindowTabPanel tabPanel) {
var tabbedPane = new ChatTabbedPane();
tabbedPane.addNewTab(tabPanel);
return tabbedPane;
}
}

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.ChatToolWindowPanel">
<form xmlns="http://www.intellij.com/uidesigner/form/" version="1" bind-to-class="ee.carlrobert.codegpt.toolwindow.chat.ChatToolWindowTabPanel">
<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

@ -0,0 +1,241 @@
package ee.carlrobert.codegpt.toolwindow.chat;
import static ee.carlrobert.codegpt.util.SwingUtils.createIconLabel;
import static ee.carlrobert.codegpt.util.SwingUtils.createTextPane;
import static ee.carlrobert.codegpt.util.SwingUtils.justifyLeft;
import static java.lang.String.format;
import com.intellij.icons.AllIcons;
import com.intellij.openapi.options.ShowSettingsUtil;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.roots.ui.componentsList.components.ScrollablePanel;
import com.intellij.ui.JBColor;
import com.intellij.ui.components.JBScrollPane;
import ee.carlrobert.codegpt.account.AccountDetailsState;
import ee.carlrobert.codegpt.conversations.Conversation;
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.components.GenerateButton;
import ee.carlrobert.codegpt.toolwindow.components.LandingView;
import ee.carlrobert.codegpt.toolwindow.components.ScrollPane;
import ee.carlrobert.codegpt.toolwindow.components.SyntaxTextArea;
import ee.carlrobert.codegpt.toolwindow.components.TextArea;
import icons.Icons;
import java.awt.Cursor;
import java.awt.Dimension;
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;
import javax.swing.Icon;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JScrollBar;
import javax.swing.JScrollPane;
import javax.swing.JTextArea;
import javax.swing.SwingUtilities;
import org.fife.ui.rsyntaxtextarea.SyntaxConstants;
import org.jetbrains.annotations.NotNull;
public class ChatToolWindowTabPanel {
private static final List<SyntaxTextArea> textAreas = new ArrayList<>();
private final Project project;
private JPanel chatGptToolWindowContent;
private ScrollPane scrollPane;
private ScrollablePanel scrollablePanel;
private JTextArea textArea;
private JScrollPane textAreaScrollPane;
private GenerateButton generateButton;
private boolean isLandingViewVisible;
private UUID conversationId;
public ChatToolWindowTabPanel(@NotNull Project project) {
this.project = project;
}
public JPanel getContent() {
return chatGptToolWindowContent;
}
public void displayUserMessage(String userMessage) {
addIconLabel(AllIcons.General.User, AccountDetailsState.getInstance().accountName);
scrollablePanel.add(createTextPane(userMessage));
scrollablePanel.revalidate();
scrollablePanel.repaint();
}
public void displayLandingView() {
if (!isLandingViewVisible) {
SwingUtilities.invokeLater(() -> {
clearWindow();
isLandingViewVisible = true;
addSpacing(16);
var landingView = new LandingView();
scrollablePanel.add(landingView.createImageIconPanel());
addSpacing(16);
landingView.getQuestionPanels().forEach(panel -> {
scrollablePanel.add(panel);
addSpacing(16);
});
scrollablePanel.revalidate();
scrollablePanel.repaint();
});
}
}
public void displayConversation(Conversation conversation) {
setConversationId(conversation.getId());
clearWindow();
conversation.getMessages().forEach(message -> {
displayUserMessage(message.getPrompt());
addIconLabel(Icons.DefaultImageIcon, "ChatGPT");
var textArea = new SyntaxTextArea(true, false, SyntaxConstants.SYNTAX_STYLE_MARKDOWN);
textArea.setText(message.getResponse());
textArea.displayCopyButton();
scrollablePanel.add(textArea);
textAreas.add(textArea);
});
scrollToBottom();
scrollablePanel.revalidate();
scrollablePanel.repaint();
}
public void sendMessage(String prompt, Project project) {
sendMessage(prompt, project, false);
}
public void sendMessage(String prompt, Project project, boolean isRetry) {
if (!isRetry) {
addIconLabel(Icons.DefaultImageIcon, "ChatGPT");
}
var settings = SettingsState.getInstance();
if (settings.apiKey.isEmpty()) {
notifyMissingCredential(project, "API key not provided.");
} else {
SyntaxTextArea textArea;
if (isRetry) {
textArea = textAreas.get(textAreas.size() - 1);
textArea.clear();
} else {
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, this, conversation);
}
}
public void clearWindow() {
isLandingViewVisible = false;
generateButton.setVisible(false);
textAreas.clear();
scrollablePanel.removeAll();
}
public void addSpacing(int height) {
scrollablePanel.add(Box.createVerticalStrut(height));
}
public void addIconLabel(Icon icon, String text) {
addSpacing(8);
scrollablePanel.add(justifyLeft(createIconLabel(icon, text)));
addSpacing(8);
}
public void notifyMissingCredential(Project project, String text) {
var label = new JLabel(format("<html>%s <font color='#589df6'><u>Open Settings</u></font> to set one.</html>", text));
label.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR));
label.addMouseListener(new MouseAdapter() {
public void mouseClicked(MouseEvent e) {
ShowSettingsUtil.getInstance().showSettingsDialog(project, SettingsConfigurable.class);
}
});
scrollablePanel.add(justifyLeft(label));
}
public void displayGenerateButton(Runnable onClick) {
generateButton.setVisible(true);
generateButton.setMode(GenerateButton.Mode.STOP, onClick);
}
public void stopGenerating(String prompt, SyntaxTextArea textArea, Project project) {
generateButton.setMode(GenerateButton.Mode.REFRESH, () -> {
sendMessage(prompt, project, true);
scrollToBottom();
});
textArea.displayCopyButton();
textArea.getCaret().setVisible(false);
scrollToBottom();
}
public void scrollToBottom() {
scrollPane.scrollToBottom();
}
public void addTextArea(SyntaxTextArea textArea) {
scrollablePanel.add(textArea);
textAreas.add(textArea);
}
public void changeStyle() {
for (var textArea : textAreas) {
textArea.changeStyleViaThemeXml();
}
}
private void handleSubmit() {
var searchText = textArea.getText();
if (isLandingViewVisible || ConversationsState.getCurrentConversation() == null) {
clearWindow();
}
displayUserMessage(searchText);
sendMessage(searchText, project);
textArea.setText("");
scrollToBottom();
scrollablePanel.revalidate();
scrollablePanel.repaint();
}
private void createUIComponents() {
textAreaScrollPane = new JBScrollPane() {
public JScrollBar createVerticalScrollBar() {
JScrollBar verticalScrollBar = new JScrollPane.ScrollBar(1);
verticalScrollBar.setPreferredSize(new Dimension(0, 0));
return verticalScrollBar;
}
};
textAreaScrollPane.setBorder(null);
textAreaScrollPane.setViewportBorder(null);
textAreaScrollPane.setBorder(BorderFactory.createCompoundBorder(
BorderFactory.createMatteBorder(1, 0, 0, 0, JBColor.border()),
BorderFactory.createEmptyBorder(0, 5, 0, 10)));
textArea = new TextArea(this::handleSubmit, textAreaScrollPane);
textAreaScrollPane.setViewportView(textArea);
scrollablePanel = new ScrollablePanel();
scrollablePanel.setLayout(new BoxLayout(scrollablePanel, BoxLayout.Y_AXIS));
scrollPane = new ScrollPane(scrollablePanel);
generateButton = new GenerateButton();
}
public UUID getConversationId() {
return conversationId;
}
public void setConversationId(UUID conversationId) {
this.conversationId = conversationId;
}
}

View file

@ -3,7 +3,6 @@ package ee.carlrobert.codegpt.toolwindow.chat.actions;
import com.intellij.icons.AllIcons;
import com.intellij.openapi.actionSystem.AnAction;
import com.intellij.openapi.actionSystem.AnActionEvent;
import ee.carlrobert.codegpt.conversations.ConversationsState;
import org.jetbrains.annotations.NotNull;
public class CreateNewConversationAction extends AnAction {
@ -19,7 +18,6 @@ public class CreateNewConversationAction extends AnAction {
public void actionPerformed(@NotNull AnActionEvent event) {
var project = event.getProject();
if (project != null) {
ConversationsState.getInstance().startConversation();
onCreate.run();
}
}

View file

@ -12,8 +12,8 @@ import com.intellij.util.ui.JBUI;
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.chat.ChatToolWindowPanel;
import ee.carlrobert.codegpt.toolwindow.chat.ChatContentManagerService;
import ee.carlrobert.codegpt.toolwindow.chat.ChatToolWindowTabPanel;
import ee.carlrobert.codegpt.toolwindow.conversations.actions.ClearAllConversationsAction;
import ee.carlrobert.codegpt.toolwindow.conversations.actions.DeleteConversationAction;
import ee.carlrobert.codegpt.toolwindow.conversations.actions.MoveDownAction;
@ -77,16 +77,14 @@ public class ConversationsToolWindow {
ConversationsState.getInstance().setCurrentConversation(conversation);
changeSettings(conversation);
var contentManagerService = project.getService(ContentManagerService.class);
var contentManagerService = project.getService(ChatContentManagerService.class);
contentManagerService.displayChatTab(project);
contentManagerService.tryFindChatTabbedPane(project)
.ifPresent(tabbedPane -> tabbedPane.tryFindActiveConversationIndex(conversation.getId())
.ifPresentOrElse(tabbedPane::setSelectedIndex, () -> {
var panel = new ChatToolWindowPanel(project);
var panel = new ChatToolWindowTabPanel(project);
panel.displayConversation(conversation);
tabbedPane.addTab("Chat " + (tabbedPane.getTabCount() + 1), panel);
tabbedPane.setSelectedIndex(tabbedPane.getTabCount() - 1);
tabbedPane.addNewTab(panel);
}));
});

View file

@ -1,12 +1,14 @@
package ee.carlrobert.codegpt.toolwindow.conversations.actions;
import static icons.Icons.DefaultImageIcon;
import com.intellij.icons.AllIcons;
import com.intellij.openapi.actionSystem.AnAction;
import com.intellij.openapi.actionSystem.AnActionEvent;
import com.intellij.openapi.ui.Messages;
import ee.carlrobert.codegpt.conversations.ConversationsState;
import ee.carlrobert.codegpt.toolwindow.chat.ChatContentManagerService;
import org.jetbrains.annotations.NotNull;
import static icons.Icons.DefaultImageIcon;
public class ClearAllConversationsAction extends AnAction {
@ -22,6 +24,11 @@ public class ClearAllConversationsAction extends AnAction {
int answer = Messages.showYesNoDialog("Are you sure you want to delete all conversations?", "Clear History", DefaultImageIcon);
if (answer == Messages.YES) {
ConversationsState.getInstance().clearAll();
var project = event.getProject();
if (project != null) {
var contentManagerService = project.getService(ChatContentManagerService.class);
contentManagerService.resetTabbedPane(project);
}
this.onRefresh.run();
}
}

View file

@ -86,7 +86,7 @@
<applicationService serviceImplementation="ee.carlrobert.codegpt.conversations.ConversationsState"/>
<applicationService serviceImplementation="ee.carlrobert.codegpt.account.AccountDetailsState"/>
<projectService serviceImplementation="ee.carlrobert.codegpt.toolwindow.ToolWindowService"/>
<projectService serviceImplementation="ee.carlrobert.codegpt.toolwindow.ContentManagerService"/>
<projectService serviceImplementation="ee.carlrobert.codegpt.toolwindow.chat.ChatContentManagerService"/>
<toolWindow id="CodeGPT" icon="Icons.ToolWindowIcon" anchor="right"
factoryClass="ee.carlrobert.codegpt.toolwindow.ProjectToolWindowFactory"/>
</extensions>