DevoxxGenieIDEAPlugin
DevoxxGenieIDEAPlugin copied to clipboard
[BOUNTY ~ 975 € Combi ticket for Devoxx Belgium 2024] : Support conversation history
Prompt you can use:
"Implement a 'Conversation History' feature for the DevoxxGenie IDEA plugin. This feature should allow users to view and manage their past conversations with the AI assistant."
We'll need to modify several components of the existing codebase. Here's a step-by-step approach to implement this feature:
- Create a Conversation model
- Modify the ChatMemoryService to store multiple conversations
- Update the UI to display conversation history
- Implement conversation switching functionality
- Add persistence for conversations
Let's go through each step with code examples:
- Create a Conversation model:
First, let's create a new class to represent a conversation:
package com.devoxx.genie.model;
import dev.langchain4j.data.message.ChatMessage;
import lombok.Data;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
@Data
public class Conversation {
private String id;
private String title;
private LocalDateTime createdAt;
private List<ChatMessage> messages;
public Conversation() {
this.id = UUID.randomUUID().toString();
this.createdAt = LocalDateTime.now();
this.messages = new ArrayList<>();
}
public void addMessage(ChatMessage message) {
messages.add(message);
}
}
- Modify the ChatMemoryService:
Update the ChatMemoryService to store multiple conversations:
package com.devoxx.genie.service;
import com.devoxx.genie.model.Conversation;
import com.intellij.openapi.application.ApplicationManager;
import dev.langchain4j.data.message.ChatMessage;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
public class ChatMemoryService {
private List<Conversation> conversations;
private Conversation currentConversation;
public static ChatMemoryService getInstance() {
return ApplicationManager.getApplication().getService(ChatMemoryService.class);
}
public ChatMemoryService() {
conversations = new ArrayList<>();
startNewConversation();
}
public void startNewConversation() {
currentConversation = new Conversation();
conversations.add(currentConversation);
}
public void addMessage(ChatMessage message) {
currentConversation.addMessage(message);
}
public List<ChatMessage> getCurrentConversationMessages() {
return currentConversation.getMessages();
}
public List<Conversation> getConversations() {
return conversations;
}
public void setCurrentConversation(String conversationId) {
Optional<Conversation> conversation = conversations.stream()
.filter(c -> c.getId().equals(conversationId))
.findFirst();
conversation.ifPresent(value -> currentConversation = value);
}
public void deleteConversation(String conversationId) {
conversations.removeIf(c -> c.getId().equals(conversationId));
if (currentConversation.getId().equals(conversationId)) {
startNewConversation();
}
}
}
- Update the UI to display conversation history:
Modify the DevoxxGenieToolWindowContent class to include a list of conversations:
public class DevoxxGenieToolWindowContent {
// ... existing fields
private JList<Conversation> conversationList;
private DefaultListModel<Conversation> conversationListModel;
// ... existing methods
private void setupUI() {
// ... existing UI setup
conversationListModel = new DefaultListModel<>();
conversationList = new JList<>(conversationListModel);
conversationList.setCellRenderer(new ConversationListCellRenderer());
conversationList.addListSelectionListener(e -> switchConversation());
JScrollPane conversationScrollPane = new JBScrollPane(conversationList);
contentPanel.add(conversationScrollPane, BorderLayout.WEST);
updateConversationList();
}
private void updateConversationList() {
conversationListModel.clear();
for (Conversation conversation : ChatMemoryService.getInstance().getConversations()) {
conversationListModel.addElement(conversation);
}
}
private void switchConversation() {
Conversation selectedConversation = conversationList.getSelectedValue();
if (selectedConversation != null) {
ChatMemoryService.getInstance().setCurrentConversation(selectedConversation.getId());
// Update the chat display with the selected conversation
updateChatDisplay();
}
}
private void updateChatDisplay() {
// Clear existing chat display
promptOutputPanel.clear();
// Add messages from the current conversation
for (ChatMessage message : ChatMemoryService.getInstance().getCurrentConversationMessages()) {
if (message instanceof UserMessage) {
promptOutputPanel.addUserPrompt(createChatMessageContext((UserMessage) message));
} else if (message instanceof AiMessage) {
promptOutputPanel.addChatResponse(createChatMessageContext((AiMessage) message));
}
}
}
private ChatMessageContext createChatMessageContext(ChatMessage message) {
// Create a ChatMessageContext from the ChatMessage
// This is a simplified version, you'll need to adapt it based on your actual ChatMessageContext structure
return ChatMessageContext.builder()
.name(String.valueOf(System.currentTimeMillis()))
.project(project)
.userPrompt(message instanceof UserMessage ? ((UserMessage) message).text() : "")
.aiMessage(message instanceof AiMessage ? (AiMessage) message : null)
.build();
}
}
- Implement conversation switching functionality:
This is partially implemented in the switchConversation() method above. You'll need to ensure that when a user selects a conversation from the list, the chat display updates accordingly.
- Add persistence for conversations:
To persist conversations between plugin restarts, you can use IntelliJ's PersistentStateComponent. Create a new class:
package com.devoxx.genie.state;
import com.devoxx.genie.model.Conversation;
import com.intellij.openapi.components.PersistentStateComponent;
import com.intellij.openapi.components.State;
import com.intellij.openapi.components.Storage;
import com.intellij.util.xmlb.XmlSerializerUtil;
import java.util.ArrayList;
import java.util.List;
@State(
name = "DevoxxGenieConversations",
storages = @Storage("devoxxGenieConversations.xml")
)
public class ConversationPersistentState implements PersistentStateComponent<ConversationPersistentState> {
public List<Conversation> conversations = new ArrayList<>();
@Override
public ConversationPersistentState getState() {
return this;
}
@Override
public void loadState(ConversationPersistentState state) {
XmlSerializerUtil.copyBean(state, this);
}
}
Then, modify the ChatMemoryService to use this persistent state:
public class ChatMemoryService {
private final ConversationPersistentState persistentState;
public ChatMemoryService(ConversationPersistentState persistentState) {
this.persistentState = persistentState;
this.conversations = persistentState.conversations;
if (conversations.isEmpty()) {
startNewConversation();
} else {
currentConversation = conversations.get(conversations.size() - 1);
}
}
// ... other methods
public void saveConversations() {
persistentState.conversations = new ArrayList<>(conversations);
}
}
Remember to call saveConversations() whenever you modify the conversations (e.g., after adding a message, starting a new conversation, or deleting a conversation).
Lastly, you'll need to update the plugin.xml file to register the persistent state component:
<applicationService serviceImplementation="com.devoxx.genie.state.ConversationPersistentState"/>
This implementation provides a basic structure for conversation history. You may need to adjust it based on your specific requirements and the existing codebase. Also, consider adding features like renaming conversations, limiting the number of stored conversations, and optimizing performance for large conversation histories.
To implement a conversation history view similar to the one shown in the image, we'll need to modify the DevoxxGenieToolWindowContent class and create a new panel for displaying recent conversations. Here's a step-by-step approach:
- Create a new RecentConversationsPanel class:
public class RecentConversationsPanel extends JPanel {
private final JList<Conversation> conversationList;
private final DefaultListModel<Conversation> listModel;
public RecentConversationsPanel() {
setLayout(new BorderLayout());
setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10));
JLabel titleLabel = new JLabel("Recent Conversations");
titleLabel.setFont(titleLabel.getFont().deriveFont(Font.BOLD, 14));
add(titleLabel, BorderLayout.NORTH);
listModel = new DefaultListModel<>();
conversationList = new JList<>(listModel);
conversationList.setCellRenderer(new ConversationListCellRenderer());
JScrollPane scrollPane = new JBScrollPane(conversationList);
add(scrollPane, BorderLayout.CENTER);
JButton viewAllButton = new JButton("View all Conversations");
viewAllButton.addActionListener(e -> viewAllConversations());
add(viewAllButton, BorderLayout.SOUTH);
}
public void updateConversationList(List<Conversation> conversations) {
listModel.clear();
for (Conversation conversation : conversations) {
listModel.addElement(conversation);
}
}
private void viewAllConversations() {
// Implement logic to show all conversations
}
private class ConversationListCellRenderer extends DefaultListCellRenderer {
@Override
public Component getListCellRendererComponent(JList<?> list, Object value, int index, boolean isSelected, boolean cellHasFocus) {
JLabel label = (JLabel) super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus);
if (value instanceof Conversation conversation) {
label.setIcon(AllIcons.General.Conversation); // Use appropriate icon
label.setText(conversation.getTitle());
label.setToolTipText(formatTimeSince(conversation.getCreatedAt()));
}
return label;
}
}
private String formatTimeSince(LocalDateTime dateTime) {
// Implement logic to format time since conversation creation
// e.g. "45 minutes ago", "1 hour 8 minutes ago", etc.
}
}
- Modify DevoxxGenieToolWindowContent to include the RecentConversationsPanel:
public class DevoxxGenieToolWindowContent {
// ... existing fields
private RecentConversationsPanel recentConversationsPanel;
private void setupUI() {
// ... existing UI setup
recentConversationsPanel = new RecentConversationsPanel();
contentPanel.add(recentConversationsPanel, BorderLayout.WEST);
updateRecentConversations();
}
private void updateRecentConversations() {
List<Conversation> recentConversations = ChatMemoryService.getInstance().getRecentConversations(5);
recentConversationsPanel.updateConversationList(recentConversations);
}
// ... other methods
}
- Update the ChatMemoryService to support retrieving recent conversations:
public class ChatMemoryService {
// ... existing methods
public List<Conversation> getRecentConversations(int limit) {
return conversations.stream()
.sorted(Comparator.comparing(Conversation::getCreatedAt).reversed())
.limit(limit)
.collect(Collectors.toList());
}
}
- Implement conversation switching when a conversation is selected:
In the RecentConversationsPanel class, add a method to set a conversation selection listener:
public void setConversationSelectionListener(Consumer<Conversation> listener) {
conversationList.addListSelectionListener(e -> {
if (!e.getValueIsAdjusting()) {
Conversation selectedConversation = conversationList.getSelectedValue();
if (selectedConversation != null) {
listener.accept(selectedConversation);
}
}
});
}
Then in DevoxxGenieToolWindowContent, set up the listener:
private void setupUI() {
// ... existing setup
recentConversationsPanel.setConversationSelectionListener(this::switchToConversation);
}
private void switchToConversation(Conversation conversation) {
ChatMemoryService.getInstance().setCurrentConversation(conversation.getId());
updateChatDisplay();
}
- Implement the "View all Conversations" functionality:
This could open a new dialog or expand the recent conversations panel to show all conversations. The implementation will depend on your specific UI requirements.
Remember to update the conversation list whenever a new conversation is started or when switching between conversations. You may also want to implement a mechanism to update the conversation titles based on the content of the conversations.
This implementation provides a basic structure for displaying recent conversations. You'll need to adjust it based on your specific requirements and existing codebase structure. Also, consider adding features like conversation deletion, renaming, and optimizing performance for large numbers of conversations.
I'm taking this one... 😜
https://github.com/user-attachments/assets/22aab533-1306-4a6b-a386-71bc6680d30f
Nice feature, but still quite some bugs:
- After the response is received, it takes +10 seconds or more before the response is displayed. I do not know whether it is because of these changes.
- Switching to a another project (same window) causes to duplicate the history with empty lines. Clicking the empty lines is not possible, so that is already ok ;-) I can remove the empty lines via the bin.
- Had one item in the history after starting the IDE. Added a new prompt and this one is duplicated three times
4.New chat -> restore a prompt/response from the history -> Delete the prompt -> this response is not removed (with a 'prompt' the prompt and response is removed)
- Got the following exception multiple times. It seems not to damage anything though
com.intellij.platform.instanceContainer.internal.ContainerDisposedException: Container 'ProjectImpl@17573199 services' was disposed
at com.intellij.platform.instanceContainer.internal.InstanceContainerImpl.state(InstanceContainerImpl.kt:60)
at com.intellij.platform.instanceContainer.internal.InstanceContainerImpl.state(InstanceContainerImpl.kt:40)
at com.intellij.platform.instanceContainer.internal.InstanceContainerImpl.getInstanceHolder(InstanceContainerImpl.kt:264)
at com.intellij.serviceContainer.ComponentManagerImpl.doGetService(ComponentManagerImpl.kt:1024)
at com.intellij.serviceContainer.ComponentManagerImpl.getService(ComponentManagerImpl.kt:988)
at com.intellij.ui.docking.DockManager.getInstance(DockManager.java:22)
at com.intellij.openapi.fileEditor.impl.FileEditorManagerImpl.getDockContainer(FileEditorManagerImpl.kt:515)
at com.intellij.openapi.fileEditor.impl.FileEditorManagerImpl.getActiveSplitterSync(FileEditorManagerImpl.kt:509)
at com.intellij.openapi.fileEditor.impl.FileEditorManagerImpl.getSelectedTextEditor(FileEditorManagerImpl.kt:1469)
at com.intellij.openapi.fileEditor.impl.FileEditorManagerImpl.getSelectedTextEditor(FileEditorManagerImpl.kt:1451)
at com.devoxx.genie.ui.EditorFileButtonManager.getSelectedTextEditor(EditorFileButtonManager.java:38)
at com.devoxx.genie.util.ChatMessageContextUtil.setWindowContext(ChatMessageContextUtil.java:92)
at com.devoxx.genie.util.ChatMessageContextUtil.createContext(ChatMessageContextUtil.java:62)
at com.devoxx.genie.ui.panel.ActionButtonsPanel.validateAndPreparePrompt(ActionButtonsPanel.java:300)
at com.devoxx.genie.ui.panel.ActionButtonsPanel.onSubmitPrompt(ActionButtonsPanel.java:188)
at com.devoxx.genie.ui.panel.ActionButtonsPanel.lambda$onPromptSubmitted$12(ActionButtonsPanel.java:544)
at java.desktop/java.awt.event.InvocationEvent.dispatch(InvocationEvent.java:318)
at java.desktop/java.awt.EventQueue.dispatchEventImpl(EventQueue.java:792)
at java.desktop/java.awt.EventQueue$3.run(EventQueue.java:739)
at java.desktop/java.awt.EventQueue$3.run(EventQueue.java:733)
at java.base/java.security.AccessController.doPrivileged(AccessController.java:399)
at java.base/java.security.ProtectionDomain$JavaSecurityAccessImpl.doIntersectionPrivilege(ProtectionDomain.java:86)
at java.desktop/java.awt.EventQueue.dispatchEvent(EventQueue.java:761)
at com.intellij.ide.IdeEventQueue.defaultDispatchEvent(IdeEventQueue.kt:695)
at com.intellij.ide.IdeEventQueue._dispatchEvent$lambda$12(IdeEventQueue.kt:589)
at com.intellij.openapi.application.impl.RwLockHolder.runWithoutImplicitRead(RwLockHolder.kt:44)
at com.intellij.ide.IdeEventQueue._dispatchEvent(IdeEventQueue.kt:589)
at com.intellij.ide.IdeEventQueue.access$_dispatchEvent(IdeEventQueue.kt:72)
at com.intellij.ide.IdeEventQueue$dispatchEvent$processEventRunnable$1$1$1.compute(IdeEventQueue.kt:355)
at com.intellij.ide.IdeEventQueue$dispatchEvent$processEventRunnable$1$1$1.compute(IdeEventQueue.kt:354)
at com.intellij.openapi.progress.impl.CoreProgressManager.computePrioritized(CoreProgressManager.java:793)
at com.intellij.ide.IdeEventQueue$dispatchEvent$processEventRunnable$1$1.invoke(IdeEventQueue.kt:354)
at com.intellij.ide.IdeEventQueue$dispatchEvent$processEventRunnable$1$1.invoke(IdeEventQueue.kt:349)
at com.intellij.ide.IdeEventQueueKt.performActivity$lambda$1(IdeEventQueue.kt:1014)
at com.intellij.openapi.application.TransactionGuardImpl.performActivity(TransactionGuardImpl.java:106)
at com.intellij.ide.IdeEventQueueKt.performActivity(IdeEventQueue.kt:1014)
at com.intellij.ide.IdeEventQueue.dispatchEvent$lambda$7(IdeEventQueue.kt:349)
at com.intellij.openapi.application.impl.ApplicationImpl.runIntendedWriteActionOnCurrentThread(ApplicationImpl.java:848)
at com.intellij.ide.IdeEventQueue.dispatchEvent(IdeEventQueue.kt:391)
at java.desktop/java.awt.EventDispatchThread.pumpOneEventForFilters(EventDispatchThread.java:207)
at java.desktop/java.awt.EventDispatchThread.pumpEventsForFilter(EventDispatchThread.java:128)
at java.desktop/java.awt.EventDispatchThread.pumpEventsForHierarchy(EventDispatchThread.java:117)
at java.desktop/java.awt.EventDispatchThread.pumpEvents(EventDispatchThread.java:113)
at java.desktop/java.awt.EventDispatchThread.pumpEvents(EventDispatchThread.java:105)
at java.desktop/java.awt.EventDispatchThread.run(EventDispatchThread.java:92)
Caused by: com.intellij.platform.instanceContainer.internal.DisposalTrace
at com.intellij.platform.instanceContainer.internal.InstanceContainerImpl.dispose(InstanceContainerImpl.kt:253)
at com.intellij.serviceContainer.ComponentManagerImpl.dispose(ComponentManagerImpl.kt:1696)
at com.intellij.openapi.project.impl.ProjectImpl.dispose(ProjectImpl.kt:316)
at com.intellij.openapi.util.ObjectTree.runWithTrace(ObjectTree.java:130)
at com.intellij.openapi.util.ObjectTree.executeAll(ObjectTree.java:162)
at com.intellij.openapi.util.Disposer.dispose(Disposer.java:250)
at com.intellij.openapi.util.Disposer.dispose(Disposer.java:238)
at com.intellij.openapi.project.impl.ProjectManagerImpl.closeProject$lambda$15(ProjectManagerImpl.kt:406)
at com.intellij.openapi.application.impl.ApplicationImpl.runWriteAction(ApplicationImpl.java:975)
at com.intellij.openapi.project.impl.ProjectManagerImpl.closeProject(ProjectManagerImpl.kt:388)
at com.intellij.openapi.project.impl.ProjectManagerImpl.closeProject$default(ProjectManagerImpl.kt:318)
at com.intellij.openapi.project.impl.ProjectManagerImpl.closeAndDispose(ProjectManagerImpl.kt:418)
at com.intellij.openapi.wm.impl.CloseProjectWindowHelper.closeProjectAndShowWelcomeFrameIfNoProjectOpened(CloseProjectWindowHelper.kt:57)
at com.intellij.openapi.wm.impl.CloseProjectWindowHelper.windowClosing(CloseProjectWindowHelper.kt:44)
at com.intellij.openapi.wm.impl.ProjectFrameHelper.windowClosing(ProjectFrameHelper.kt:434)
at com.intellij.openapi.wm.impl.WindowCloseListener.windowClosing(ProjectFrameHelper.kt:454)
at java.desktop/java.awt.AWTEventMulticaster.windowClosing(AWTEventMulticaster.java:357)
at java.desktop/java.awt.AWTEventMulticaster.windowClosing(AWTEventMulticaster.java:357)
at java.desktop/java.awt.Window.processWindowEvent(Window.java:2114)
at java.desktop/javax.swing.JFrame.processWindowEvent(JFrame.java:298)
at java.desktop/java.awt.Window.processEvent(Window.java:2073)
at java.desktop/java.awt.Component.dispatchEventImpl(Component.java:5027)
at java.desktop/java.awt.Container.dispatchEventImpl(Container.java:2324)
at java.desktop/java.awt.Window.dispatchEventImpl(Window.java:2809)
at java.desktop/java.awt.Component.dispatchEvent(Component.java:4855)
at java.desktop/java.awt.EventQueue.dispatchEventImpl(EventQueue.java:794)
at java.desktop/java.awt.EventQueue$3.run(EventQueue.java:739)
at java.desktop/java.awt.EventQueue$3.run(EventQueue.java:733)
at java.base/java.security.AccessController.doPrivileged(AccessController.java:399)
at java.base/java.security.ProtectionDomain$JavaSecurityAccessImpl.doIntersectionPrivilege(ProtectionDomain.java:86)
at java.base/java.security.ProtectionDomain$JavaSecurityAccessImpl.doIntersectionPrivilege(ProtectionDomain.java:97)
at java.desktop/java.awt.EventQueue$4.run(EventQueue.java:766)
at java.desktop/java.awt.EventQueue$4.run(EventQueue.java:764)
at java.base/java.security.AccessController.doPrivileged(AccessController.java:399)
at java.base/java.security.ProtectionDomain$JavaSecurityAccessImpl.doIntersectionPrivilege(ProtectionDomain.java:86)
at java.desktop/java.awt.EventQueue.dispatchEvent(EventQueue.java:763)
at com.intellij.ide.IdeEventQueue.defaultDispatchEvent(IdeEventQueue.kt:695)
at com.intellij.ide.IdeEventQueue._dispatchEvent$lambda$12(IdeEventQueue.kt:589)
at com.intellij.openapi.application.impl.RwLockHolder.runWithoutImplicitRead(RwLockHolder.kt:44)
at com.intellij.ide.IdeEventQueue._dispatchEvent(IdeEventQueue.kt:589)
at com.intellij.ide.IdeEventQueue.access$_dispatchEvent(IdeEventQueue.kt:72)
at com.intellij.ide.IdeEventQueue$dispatchEvent$processEventRunnable$1$1$1.compute(IdeEventQueue.kt:355)
at com.intellij.ide.IdeEventQueue$dispatchEvent$processEventRunnable$1$1$1.compute(IdeEventQueue.kt:354)
at com.intellij.openapi.progress.impl.CoreProgressManager.computePrioritized(CoreProgressManager.java:793)
at com.intellij.ide.IdeEventQueue$dispatchEvent$processEventRunnable$1$1.invoke(IdeEventQueue.kt:354)
at com.intellij.ide.IdeEventQueue$dispatchEvent$processEventRunnable$1$1.invoke(IdeEventQueue.kt:349)
at com.intellij.ide.IdeEventQueueKt.performActivity$lambda$1(IdeEventQueue.kt:1014)
at com.intellij.openapi.application.TransactionGuardImpl.performActivity(TransactionGuardImpl.java:114)
... 10 more
Will investigate...
I was thinking: chats will be mostly about code and will be project specific. Shouldn't the chat history be project specific?
ChatHistory is now project related, please try to break it :)
https://github.com/user-attachments/assets/3e83d736-a9f9-429c-9c32-c0a71db115ff
Congrats, this is already much better and stable :+1: Some findings:
- New chat -> restore a prompt/response from the history -> Delete the prompt from the conversation (not the history but the conversation) -> the response is not removed (I can only remove it if I start a new chat)
- What should happen if I want to restore two history items? Currently, only one can be restored, a second one will overwrite the first restored one
- New chat -> enter a prompt and wait for the response -> restore a chat history item -> this overwrites the previous prompt/response. Probably similar problem as in 2
All Fixed and new version tagged and deployed to marketplace