mirror of
https://github.com/carlrobertoh/ProxyAI.git
synced 2026-05-21 19:13:38 +00:00
feat: improve chat history ui/ux
This commit is contained in:
parent
4122b46e1d
commit
a6ff38e52c
8 changed files with 912 additions and 202 deletions
|
|
@ -66,6 +66,7 @@ public final class ConversationService {
|
|||
public void saveMessage(@NotNull Conversation conversation, @NotNull Message message) {
|
||||
conversation.setUpdatedOn(LocalDateTime.now());
|
||||
conversation.addMessage(message);
|
||||
saveConversation(conversation);
|
||||
}
|
||||
|
||||
public void saveConversation(Conversation conversation) {
|
||||
|
|
|
|||
|
|
@ -1,114 +0,0 @@
|
|||
package ee.carlrobert.codegpt.toolwindow.conversations;
|
||||
|
||||
import com.intellij.openapi.project.Project;
|
||||
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.actions.toolwindow.DeleteConversationAction;
|
||||
import ee.carlrobert.codegpt.conversations.Conversation;
|
||||
import ee.carlrobert.codegpt.conversations.ConversationsState;
|
||||
import ee.carlrobert.codegpt.toolwindow.chat.ChatToolWindowContentManager;
|
||||
import ee.carlrobert.codegpt.ui.IconActionButton;
|
||||
import java.awt.BorderLayout;
|
||||
import java.awt.Cursor;
|
||||
import java.awt.GridBagConstraints;
|
||||
import java.awt.GridBagLayout;
|
||||
import java.awt.event.MouseAdapter;
|
||||
import java.awt.event.MouseEvent;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import javax.swing.JLabel;
|
||||
import javax.swing.JPanel;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
class ConversationPanel extends JPanel {
|
||||
|
||||
ConversationPanel(
|
||||
@NotNull Project project,
|
||||
@NotNull Conversation conversation,
|
||||
@NotNull Runnable onDelete) {
|
||||
super(new BorderLayout());
|
||||
var toolWindowContentManager = project.getService(ChatToolWindowContentManager.class);
|
||||
init(toolWindowContentManager, conversation, onDelete);
|
||||
}
|
||||
|
||||
private void init(
|
||||
ChatToolWindowContentManager toolWindowContentManager,
|
||||
Conversation conversation,
|
||||
Runnable onDelete) {
|
||||
setBackground(JBColor.background());
|
||||
addMouseListener(new MouseAdapter() {
|
||||
@Override
|
||||
public void mouseClicked(MouseEvent e) {
|
||||
toolWindowContentManager.displayConversation(conversation);
|
||||
}
|
||||
});
|
||||
addStyles(isSelected(conversation));
|
||||
addTextPanel(conversation, onDelete);
|
||||
setCursor(new Cursor(Cursor.HAND_CURSOR));
|
||||
}
|
||||
|
||||
private boolean isSelected(Conversation conversation) {
|
||||
var currentConversation = ConversationsState.getCurrentConversation();
|
||||
return currentConversation != null && currentConversation.getId().equals(conversation.getId());
|
||||
}
|
||||
|
||||
private void addStyles(boolean isSelected) {
|
||||
var border = isSelected
|
||||
? JBUI.Borders.customLine(JBUI.CurrentTheme.ActionButton.focusedBorder(), 2, 2, 2, 2)
|
||||
: JBUI.Borders.customLine(JBColor.border(), 1, 0, 1, 0);
|
||||
setBorder(JBUI.Borders.compound(border, JBUI.Borders.empty(8)));
|
||||
setLayout(new GridBagLayout());
|
||||
setCursor(new Cursor(Cursor.HAND_CURSOR));
|
||||
}
|
||||
|
||||
private void addTextPanel(Conversation conversation, Runnable onDelete) {
|
||||
var constraints = new GridBagConstraints();
|
||||
constraints.gridx = 1;
|
||||
constraints.weightx = 1.0;
|
||||
constraints.fill = GridBagConstraints.HORIZONTAL;
|
||||
add(createTextPanel(conversation, onDelete), constraints);
|
||||
}
|
||||
|
||||
private JPanel createTextPanel(Conversation conversation, Runnable onDelete) {
|
||||
var headerPanel = new JPanel(new GridBagLayout());
|
||||
headerPanel.setBorder(JBUI.Borders.emptyBottom(12));
|
||||
|
||||
var gbc = new GridBagConstraints();
|
||||
gbc.fill = GridBagConstraints.HORIZONTAL;
|
||||
gbc.weightx = 1.0;
|
||||
gbc.gridx = 0;
|
||||
|
||||
headerPanel.add(new JBLabel(getConversationDisplayTitle(conversation))
|
||||
.withFont(JBFont.label().asBold()), gbc);
|
||||
|
||||
gbc.gridx = 1;
|
||||
gbc.weightx = 0;
|
||||
headerPanel.add(new IconActionButton(new DeleteConversationAction(onDelete), "DELETE"), gbc);
|
||||
|
||||
var bottomPanel = new JPanel(new BorderLayout());
|
||||
bottomPanel.add(new JLabel(conversation.getUpdatedOn()
|
||||
.format(DateTimeFormatter.ofPattern("M/d/yyyy, h:mm:ss a"))), BorderLayout.WEST);
|
||||
|
||||
var textPanel = new JPanel(new BorderLayout());
|
||||
textPanel.add(headerPanel, BorderLayout.NORTH);
|
||||
textPanel.add(bottomPanel, BorderLayout.SOUTH);
|
||||
return textPanel;
|
||||
}
|
||||
|
||||
private String getConversationDisplayTitle(Conversation conversation) {
|
||||
String title = conversation.getTitle();
|
||||
if (title != null && !title.trim().isEmpty()) {
|
||||
return title;
|
||||
}
|
||||
return getFirstPrompt(conversation);
|
||||
}
|
||||
|
||||
private String getFirstPrompt(Conversation conversation) {
|
||||
var messages = conversation.getMessages();
|
||||
if (messages.isEmpty()) {
|
||||
return "";
|
||||
}
|
||||
return messages.get(0).getPrompt();
|
||||
}
|
||||
}
|
||||
|
|
@ -1,83 +0,0 @@
|
|||
package ee.carlrobert.codegpt.toolwindow.conversations;
|
||||
|
||||
import com.intellij.openapi.actionSystem.ActionManager;
|
||||
import com.intellij.openapi.actionSystem.DefaultActionGroup;
|
||||
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.components.JBScrollPane;
|
||||
import com.intellij.util.ui.JBFont;
|
||||
import com.intellij.util.ui.JBUI;
|
||||
import ee.carlrobert.codegpt.actions.toolwindow.DeleteAllConversationsAction;
|
||||
import ee.carlrobert.codegpt.actions.toolwindow.MoveDownAction;
|
||||
import ee.carlrobert.codegpt.actions.toolwindow.MoveUpAction;
|
||||
import ee.carlrobert.codegpt.conversations.ConversationService;
|
||||
import javax.swing.Box;
|
||||
import javax.swing.BoxLayout;
|
||||
import javax.swing.JLabel;
|
||||
import javax.swing.JPanel;
|
||||
import javax.swing.JScrollPane;
|
||||
import javax.swing.ScrollPaneConstants;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
public class ConversationsToolWindow extends JPanel {
|
||||
|
||||
private final Project project;
|
||||
private final ConversationService conversationService;
|
||||
private final ScrollablePanel scrollablePanel;
|
||||
private final JScrollPane scrollPane;
|
||||
|
||||
public ConversationsToolWindow(@NotNull Project project) {
|
||||
this.project = project;
|
||||
this.conversationService = ConversationService.getInstance();
|
||||
scrollablePanel = new ScrollablePanel();
|
||||
scrollablePanel.setLayout(new BoxLayout(scrollablePanel, BoxLayout.Y_AXIS));
|
||||
|
||||
scrollPane = new JBScrollPane();
|
||||
scrollPane.setHorizontalScrollBarPolicy(ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER);
|
||||
scrollPane.setViewportView(scrollablePanel);
|
||||
scrollPane.setBorder(null);
|
||||
scrollPane.setViewportBorder(null);
|
||||
refresh();
|
||||
}
|
||||
|
||||
public JPanel getContent() {
|
||||
SimpleToolWindowPanel panel = new SimpleToolWindowPanel(true);
|
||||
panel.setContent(scrollPane);
|
||||
|
||||
var actionGroup = new DefaultActionGroup("TOOLBAR_ACTION_GROUP", false);
|
||||
actionGroup.add(new MoveDownAction(this::refresh));
|
||||
actionGroup.add(new MoveUpAction(this::refresh));
|
||||
actionGroup.addSeparator();
|
||||
actionGroup.add(new DeleteAllConversationsAction(this::refresh));
|
||||
|
||||
var toolbar = ActionManager.getInstance()
|
||||
.createActionToolbar("NAVIGATION_BAR_TOOLBAR", actionGroup, true);
|
||||
toolbar.setTargetComponent(panel);
|
||||
panel.setToolbar(toolbar.getComponent());
|
||||
return panel;
|
||||
}
|
||||
|
||||
public void refresh() {
|
||||
scrollablePanel.removeAll();
|
||||
|
||||
var sortedConversations = conversationService.getSortedConversations();
|
||||
if (sortedConversations.isEmpty()) {
|
||||
var emptyLabel = new JLabel("No conversations exist.");
|
||||
emptyLabel.setFont(JBFont.h2());
|
||||
emptyLabel.setBorder(JBUI.Borders.empty(8));
|
||||
scrollablePanel.add(emptyLabel);
|
||||
} else {
|
||||
sortedConversations.forEach(conversation -> {
|
||||
scrollablePanel.add(Box.createVerticalStrut(8));
|
||||
scrollablePanel.add(new ConversationPanel(project, conversation, () -> {
|
||||
ConversationService.getInstance().deleteConversation(conversation);
|
||||
refresh();
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
||||
scrollablePanel.revalidate();
|
||||
scrollablePanel.repaint();
|
||||
}
|
||||
}
|
||||
|
|
@ -5,9 +5,9 @@ import com.intellij.openapi.project.Project
|
|||
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.content.ContentManagerListener
|
||||
import ee.carlrobert.codegpt.toolwindow.chat.ChatToolWindowPanel
|
||||
import ee.carlrobert.codegpt.toolwindow.conversations.ConversationsToolWindow
|
||||
import ee.carlrobert.codegpt.toolwindow.history.ChatHistoryToolWindow
|
||||
import javax.swing.JComponent
|
||||
|
||||
class ProxyAIToolWindowFactory : ToolWindowFactory, DumbAware {
|
||||
|
|
@ -17,14 +17,15 @@ class ProxyAIToolWindowFactory : ToolWindowFactory, DumbAware {
|
|||
toolWindow: ToolWindow
|
||||
) {
|
||||
var chatToolWindowPanel = ChatToolWindowPanel(project, toolWindow.disposable)
|
||||
var conversationsToolWindow = ConversationsToolWindow(project)
|
||||
var chatHistoryToolWindow = ChatHistoryToolWindow(project)
|
||||
|
||||
addContent(toolWindow, chatToolWindowPanel, "Chat")
|
||||
addContent(toolWindow, conversationsToolWindow.getContent(), "Chat History")
|
||||
addContent(toolWindow, chatHistoryToolWindow.getContent(), "Chat History")
|
||||
|
||||
toolWindow.addContentManagerListener(object : ContentManagerListener {
|
||||
override fun selectionChanged(event: ContentManagerEvent) {
|
||||
if ("Chat History" == event.content.tabName && event.content.isSelected) {
|
||||
conversationsToolWindow.refresh()
|
||||
chatHistoryToolWindow.refresh()
|
||||
}
|
||||
}
|
||||
})
|
||||
|
|
|
|||
|
|
@ -0,0 +1,271 @@
|
|||
package ee.carlrobert.codegpt.toolwindow.history
|
||||
|
||||
import com.intellij.icons.AllIcons
|
||||
import com.intellij.ui.JBColor
|
||||
import com.intellij.ui.components.JBLabel
|
||||
import com.intellij.ui.dsl.builder.*
|
||||
import com.intellij.util.ui.JBFont
|
||||
import com.intellij.util.ui.JBUI
|
||||
import com.intellij.util.ui.UIUtil
|
||||
import com.intellij.util.ui.components.BorderLayoutPanel
|
||||
import ee.carlrobert.codegpt.CodeGPTBundle
|
||||
import ee.carlrobert.codegpt.conversations.Conversation
|
||||
import java.awt.Color
|
||||
import java.awt.Cursor
|
||||
import java.awt.Dimension
|
||||
import java.awt.event.MouseAdapter
|
||||
import java.awt.event.MouseEvent
|
||||
import java.time.format.DateTimeFormatter
|
||||
import javax.swing.JButton
|
||||
import javax.swing.JPanel
|
||||
import javax.swing.SwingUtilities
|
||||
import kotlin.math.max
|
||||
|
||||
class ChatHistoryItemPanel(
|
||||
private val conversation: Conversation,
|
||||
private val onClicked: () -> Unit,
|
||||
private val onDoubleClicked: () -> Unit,
|
||||
private val onDeleteClicked: () -> Unit,
|
||||
private var isSelected: Boolean = false
|
||||
) : BorderLayoutPanel() {
|
||||
|
||||
companion object {
|
||||
private val DATE_FORMATTER = DateTimeFormatter.ofPattern("MMM d, h:mm a")
|
||||
private const val TITLE_MAX_LENGTH = 60
|
||||
private const val PREVIEW_MAX_LENGTH = 100
|
||||
private const val MIN_PANEL_HEIGHT = 80
|
||||
private const val BUTTON_SIZE = 20
|
||||
}
|
||||
|
||||
private val deleteButton: JButton = createDeleteButton()
|
||||
private var isHovered = false
|
||||
|
||||
init {
|
||||
setupUI()
|
||||
setupMouseListeners()
|
||||
}
|
||||
|
||||
fun setSelected(selected: Boolean) {
|
||||
if (isSelected != selected) {
|
||||
isSelected = selected
|
||||
setupBackground()
|
||||
repaint()
|
||||
}
|
||||
}
|
||||
|
||||
override fun getPreferredSize(): Dimension {
|
||||
val size = super.getPreferredSize()
|
||||
return Dimension(size.width, max(size.height, MIN_PANEL_HEIGHT))
|
||||
}
|
||||
|
||||
private fun setupUI() {
|
||||
cursor = Cursor(Cursor.HAND_CURSOR)
|
||||
border = JBUI.Borders.customLine(JBColor.border(), 0, 0, 1, 0)
|
||||
setupBackground()
|
||||
addToCenter(createMainPanel())
|
||||
}
|
||||
|
||||
private fun setupMouseListeners() {
|
||||
addMouseListener(object : MouseAdapter() {
|
||||
override fun mouseClicked(e: MouseEvent) {
|
||||
when (e.clickCount) {
|
||||
1 -> onClicked()
|
||||
2 -> onDoubleClicked()
|
||||
}
|
||||
}
|
||||
|
||||
override fun mouseEntered(e: MouseEvent) {
|
||||
isHovered = true
|
||||
setupBackground()
|
||||
repaint()
|
||||
deleteButton.icon = AllIcons.Actions.Close
|
||||
deleteButton.rolloverIcon = AllIcons.Actions.CloseHovered
|
||||
}
|
||||
|
||||
override fun mouseExited(e: MouseEvent) {
|
||||
val point =
|
||||
SwingUtilities.convertPoint(e.component, e.point, this@ChatHistoryItemPanel)
|
||||
if (!contains(point)) {
|
||||
isHovered = false
|
||||
setupBackground()
|
||||
repaint()
|
||||
deleteButton.icon = null
|
||||
deleteButton.rolloverIcon = null
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private fun setupBackground() {
|
||||
background = when {
|
||||
isSelected -> UIUtil.getListSelectionBackground(true)
|
||||
isHovered -> UIUtil.getListBackground().darker()
|
||||
else -> UIUtil.getListBackground()
|
||||
}
|
||||
}
|
||||
|
||||
private fun createDeleteButton(): JButton {
|
||||
return JButton().apply {
|
||||
isOpaque = false
|
||||
isContentAreaFilled = false
|
||||
isBorderPainted = false
|
||||
isFocusPainted = false
|
||||
toolTipText = CodeGPTBundle.get("conversation.deleteButton.tooltip")
|
||||
preferredSize = Dimension(BUTTON_SIZE, BUTTON_SIZE)
|
||||
minimumSize = Dimension(BUTTON_SIZE, BUTTON_SIZE)
|
||||
maximumSize = Dimension(BUTTON_SIZE, BUTTON_SIZE)
|
||||
icon = null
|
||||
|
||||
addActionListener {
|
||||
onDeleteClicked()
|
||||
}
|
||||
|
||||
isVisible = true
|
||||
|
||||
addMouseListener(object : MouseAdapter() {
|
||||
override fun mouseEntered(e: MouseEvent) {
|
||||
isHovered = true
|
||||
setupBackground()
|
||||
this@ChatHistoryItemPanel.repaint()
|
||||
}
|
||||
|
||||
override fun mouseExited(e: MouseEvent) {
|
||||
val parentPoint =
|
||||
SwingUtilities.convertPoint(e.component, e.point, this@ChatHistoryItemPanel)
|
||||
if (!this@ChatHistoryItemPanel.contains(parentPoint)) {
|
||||
isHovered = false
|
||||
setupBackground()
|
||||
this@ChatHistoryItemPanel.repaint()
|
||||
icon = null
|
||||
rolloverIcon = null
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
private fun createMainPanel(): JPanel {
|
||||
return panel {
|
||||
row {
|
||||
cell(createTitlePanel())
|
||||
.align(Align.FILL)
|
||||
}.resizableRow()
|
||||
|
||||
val previewText = getPreviewText()
|
||||
if (previewText.isNotEmpty()) {
|
||||
row {
|
||||
cell(createPreviewLabel(previewText))
|
||||
.align(Align.FILL)
|
||||
}.topGap(TopGap.NONE)
|
||||
}
|
||||
|
||||
row {
|
||||
cell(createMetadataPanel())
|
||||
.align(Align.FILL)
|
||||
}.topGap(TopGap.NONE)
|
||||
}.apply {
|
||||
isOpaque = false
|
||||
border = JBUI.Borders.empty(4, 8)
|
||||
}
|
||||
}
|
||||
|
||||
private fun createTitlePanel(): JPanel {
|
||||
return panel {
|
||||
row {
|
||||
cell(createTitleLabel())
|
||||
.align(AlignX.FILL)
|
||||
.resizableColumn()
|
||||
cell(deleteButton)
|
||||
.align(AlignY.CENTER)
|
||||
}
|
||||
}.apply {
|
||||
isOpaque = false
|
||||
border = JBUI.Borders.empty(2, 0, 2, 0)
|
||||
}
|
||||
}
|
||||
|
||||
private fun createPreviewLabel(previewText: String): JBLabel {
|
||||
return JBLabel(previewText).apply {
|
||||
font = JBFont.regular().deriveFont(12f)
|
||||
foreground = getPreviewColor()
|
||||
maximumSize = Dimension(Int.MAX_VALUE, preferredSize.height * 2)
|
||||
}
|
||||
}
|
||||
|
||||
private fun createMetadataPanel(): JPanel {
|
||||
return panel {
|
||||
row {
|
||||
label(formatDate())
|
||||
.applyToComponent {
|
||||
font = JBFont.regular().deriveFont(11f)
|
||||
foreground = getMetadataColor()
|
||||
}
|
||||
.align(AlignX.LEFT)
|
||||
.resizableColumn()
|
||||
|
||||
val messageCount = conversation.messages.size
|
||||
if (messageCount > 0) {
|
||||
val text = if (messageCount == 1) {
|
||||
CodeGPTBundle.get("conversation.messageCount.singular", messageCount)
|
||||
} else {
|
||||
CodeGPTBundle.get("conversation.messageCount.plural", messageCount)
|
||||
}
|
||||
label(text)
|
||||
.applyToComponent {
|
||||
font = JBFont.regular().deriveFont(11f)
|
||||
foreground = getMetadataColor()
|
||||
}
|
||||
.align(AlignX.RIGHT)
|
||||
}
|
||||
}
|
||||
}.apply {
|
||||
isOpaque = false
|
||||
}
|
||||
}
|
||||
|
||||
private fun createTitleLabel(): JBLabel {
|
||||
return JBLabel(getConversationDisplayTitle()).apply {
|
||||
font = JBFont.label().deriveFont(14f).asBold()
|
||||
foreground = UIUtil.getLabelForeground()
|
||||
}
|
||||
}
|
||||
|
||||
private fun getPreviewColor(): Color {
|
||||
return JBColor(Color(128, 128, 128), Color(169, 169, 169))
|
||||
}
|
||||
|
||||
private fun getMetadataColor(): Color {
|
||||
return JBColor(Color(150, 150, 150), Color(130, 130, 130))
|
||||
}
|
||||
|
||||
private fun getConversationDisplayTitle(): String {
|
||||
val title = conversation.title?.takeIf { it.isNotBlank() } ?: getFirstPrompt()
|
||||
return if (title.length > TITLE_MAX_LENGTH) {
|
||||
title.substring(0, TITLE_MAX_LENGTH - 3) + "..."
|
||||
} else {
|
||||
title
|
||||
}
|
||||
}
|
||||
|
||||
private fun getFirstPrompt(): String {
|
||||
return conversation.messages.firstOrNull()?.prompt?.trim()
|
||||
?: CodeGPTBundle.get("conversation.defaultTitle")
|
||||
}
|
||||
|
||||
private fun getPreviewText(): String {
|
||||
val lastMessage = conversation.messages.lastOrNull() ?: return ""
|
||||
val text = lastMessage.response?.trim() ?: lastMessage.prompt?.trim() ?: ""
|
||||
|
||||
val cleanedText = text.replace("\n", " ").replace(Regex("\\s+"), " ").trim()
|
||||
|
||||
return if (cleanedText.length > PREVIEW_MAX_LENGTH) {
|
||||
cleanedText.substring(0, PREVIEW_MAX_LENGTH - 3) + "..."
|
||||
} else {
|
||||
cleanedText
|
||||
}
|
||||
}
|
||||
|
||||
private fun formatDate(): String {
|
||||
return conversation.updatedOn.format(DATE_FORMATTER)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,196 @@
|
|||
package ee.carlrobert.codegpt.toolwindow.history
|
||||
|
||||
import com.intellij.ui.components.JBScrollPane
|
||||
import com.intellij.ui.dsl.builder.panel
|
||||
import com.intellij.util.ui.JBFont
|
||||
import com.intellij.util.ui.JBUI
|
||||
import com.intellij.util.ui.UIUtil
|
||||
import com.intellij.util.ui.components.BorderLayoutPanel
|
||||
import ee.carlrobert.codegpt.CodeGPTBundle
|
||||
import ee.carlrobert.codegpt.conversations.Conversation
|
||||
import java.awt.BorderLayout
|
||||
import java.awt.Rectangle
|
||||
import java.awt.event.KeyAdapter
|
||||
import java.awt.event.KeyEvent
|
||||
import java.util.*
|
||||
import javax.swing.BoxLayout
|
||||
import javax.swing.JPanel
|
||||
import javax.swing.ScrollPaneConstants
|
||||
|
||||
class ChatHistoryListPanel : BorderLayoutPanel() {
|
||||
|
||||
private val itemsPanel = JPanel()
|
||||
private var scrollPane = JBScrollPane(itemsPanel)
|
||||
private var conversations = listOf<Conversation>()
|
||||
private var selectedConversation: Conversation? = null
|
||||
private var onConversationSelected: ((Conversation?) -> Unit)? = null
|
||||
private var onConversationDoubleClicked: ((Conversation) -> Unit)? = null
|
||||
private var onConversationDeleted: ((Conversation) -> Unit)? = null
|
||||
private val conversationPanels = mutableMapOf<UUID, ChatHistoryItemPanel>()
|
||||
|
||||
init {
|
||||
setupItemsPanel()
|
||||
setupScrollPane()
|
||||
setupKeyboardSupport()
|
||||
}
|
||||
|
||||
fun setConversations(newConversations: List<Conversation>) {
|
||||
val previousSelection = selectedConversation
|
||||
conversations = newConversations
|
||||
rebuildItems()
|
||||
|
||||
previousSelection?.let { selected ->
|
||||
conversations.find { it.id == selected.id }?.let { setSelectedConversation(it) }
|
||||
}
|
||||
}
|
||||
|
||||
fun setSelectedConversation(conversation: Conversation?) {
|
||||
if (selectedConversation != conversation) {
|
||||
selectedConversation?.let { prev ->
|
||||
conversationPanels[prev.id]?.setSelected(false)
|
||||
}
|
||||
|
||||
selectedConversation = conversation
|
||||
|
||||
conversation?.let { conv ->
|
||||
conversationPanels[conv.id]?.setSelected(true)
|
||||
}
|
||||
|
||||
onConversationSelected?.invoke(conversation)
|
||||
}
|
||||
}
|
||||
|
||||
fun setOnConversationSelected(callback: (Conversation?) -> Unit) {
|
||||
onConversationSelected = callback
|
||||
}
|
||||
|
||||
fun setOnConversationDoubleClicked(callback: (Conversation) -> Unit) {
|
||||
onConversationDoubleClicked = callback
|
||||
}
|
||||
|
||||
fun setOnConversationDeleted(callback: (Conversation) -> Unit) {
|
||||
onConversationDeleted = callback
|
||||
}
|
||||
|
||||
private fun setupItemsPanel() {
|
||||
itemsPanel.layout = BoxLayout(itemsPanel, BoxLayout.Y_AXIS)
|
||||
itemsPanel.background = UIUtil.getListBackground()
|
||||
}
|
||||
|
||||
private fun setupScrollPane() {
|
||||
scrollPane.apply {
|
||||
horizontalScrollBarPolicy = ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER
|
||||
verticalScrollBarPolicy = ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED
|
||||
border = null
|
||||
viewportBorder = null
|
||||
background = UIUtil.getListBackground()
|
||||
}
|
||||
addToCenter(scrollPane)
|
||||
}
|
||||
|
||||
private fun setupKeyboardSupport() {
|
||||
isFocusable = true
|
||||
addKeyListener(object : KeyAdapter() {
|
||||
override fun keyPressed(e: KeyEvent) {
|
||||
when (e.keyCode) {
|
||||
KeyEvent.VK_UP -> selectPrevious()
|
||||
KeyEvent.VK_DOWN -> selectNext()
|
||||
KeyEvent.VK_ENTER -> selectedConversation?.let {
|
||||
onConversationDoubleClicked?.invoke(
|
||||
it
|
||||
)
|
||||
}
|
||||
|
||||
KeyEvent.VK_DELETE, KeyEvent.VK_BACK_SPACE -> {
|
||||
selectedConversation?.let { onConversationDeleted?.invoke(it) }
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private fun selectPrevious() {
|
||||
val currentIndex = selectedConversation?.let { conversations.indexOf(it) } ?: 0
|
||||
if (currentIndex > 0) {
|
||||
setSelectedConversation(conversations[currentIndex - 1])
|
||||
scrollToSelected()
|
||||
}
|
||||
}
|
||||
|
||||
private fun selectNext() {
|
||||
val currentIndex = selectedConversation?.let { conversations.indexOf(it) } ?: -1
|
||||
if (currentIndex < conversations.size - 1) {
|
||||
setSelectedConversation(conversations[currentIndex + 1])
|
||||
scrollToSelected()
|
||||
}
|
||||
}
|
||||
|
||||
private fun scrollToSelected() {
|
||||
selectedConversation?.let { selected ->
|
||||
val index = conversations.indexOf(selected)
|
||||
if (index >= 0 && index < itemsPanel.componentCount) {
|
||||
val component = itemsPanel.getComponent(index)
|
||||
scrollPane.viewport.scrollRectToVisible(
|
||||
Rectangle(0, component.y, component.width, component.height)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun rebuildItems() {
|
||||
itemsPanel.removeAll()
|
||||
|
||||
if (conversations.isEmpty()) {
|
||||
addEmptyState()
|
||||
} else {
|
||||
addConversationItems()
|
||||
}
|
||||
|
||||
itemsPanel.revalidate()
|
||||
itemsPanel.repaint()
|
||||
}
|
||||
|
||||
private fun addEmptyState() {
|
||||
val emptyPanel = panel {
|
||||
row {
|
||||
label(CodeGPTBundle.get("conversation.emptyState"))
|
||||
.applyToComponent {
|
||||
font = JBFont.regular().deriveFont(14f)
|
||||
foreground = UIUtil.getInactiveTextColor()
|
||||
}
|
||||
}
|
||||
}.apply {
|
||||
border = JBUI.Borders.empty(40)
|
||||
isOpaque = false
|
||||
}
|
||||
|
||||
itemsPanel.layout = BorderLayout()
|
||||
itemsPanel.add(emptyPanel, BorderLayout.CENTER)
|
||||
}
|
||||
|
||||
private fun addConversationItems() {
|
||||
itemsPanel.layout = BoxLayout(itemsPanel, BoxLayout.Y_AXIS)
|
||||
|
||||
conversationPanels.clear()
|
||||
|
||||
conversations.forEach { conversation ->
|
||||
val isCurrentlySelected = selectedConversation?.id == conversation.id
|
||||
val itemPanel = ChatHistoryItemPanel(
|
||||
conversation = conversation,
|
||||
onClicked = {
|
||||
setSelectedConversation(conversation)
|
||||
onConversationDoubleClicked?.invoke(conversation)
|
||||
},
|
||||
onDoubleClicked = { onConversationDoubleClicked?.invoke(conversation) },
|
||||
onDeleteClicked = { onConversationDeleted?.invoke(conversation) },
|
||||
isSelected = isCurrentlySelected
|
||||
)
|
||||
|
||||
itemPanel.alignmentX = LEFT_ALIGNMENT
|
||||
itemPanel.maximumSize =
|
||||
java.awt.Dimension(Integer.MAX_VALUE, itemPanel.preferredSize.height)
|
||||
itemsPanel.add(itemPanel)
|
||||
conversationPanels[conversation.id] = itemPanel
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,413 @@
|
|||
package ee.carlrobert.codegpt.toolwindow.history
|
||||
|
||||
import com.intellij.icons.AllIcons
|
||||
import com.intellij.openapi.actionSystem.*
|
||||
import com.intellij.openapi.project.Project
|
||||
import com.intellij.openapi.ui.SimpleToolWindowPanel
|
||||
import com.intellij.openapi.ui.popup.JBPopupFactory
|
||||
import com.intellij.openapi.util.Key
|
||||
import com.intellij.ui.DocumentAdapter
|
||||
import com.intellij.ui.SearchTextField
|
||||
import com.intellij.ui.components.JBLabel
|
||||
import com.intellij.ui.dsl.builder.AlignX
|
||||
import com.intellij.ui.dsl.builder.panel
|
||||
import com.intellij.util.ui.JBFont
|
||||
import com.intellij.util.ui.JBUI
|
||||
import com.intellij.util.ui.UIUtil
|
||||
import com.intellij.util.ui.components.BorderLayoutPanel
|
||||
import ee.carlrobert.codegpt.CodeGPTBundle
|
||||
import ee.carlrobert.codegpt.actions.toolwindow.DeleteAllConversationsAction
|
||||
import ee.carlrobert.codegpt.conversations.Conversation
|
||||
import ee.carlrobert.codegpt.conversations.ConversationService
|
||||
import ee.carlrobert.codegpt.conversations.ConversationsState
|
||||
import ee.carlrobert.codegpt.toolwindow.chat.ChatToolWindowContentManager
|
||||
import javax.swing.JOptionPane
|
||||
import javax.swing.JPanel
|
||||
import javax.swing.SwingUtilities
|
||||
import javax.swing.Timer
|
||||
import javax.swing.event.DocumentEvent
|
||||
import kotlin.concurrent.thread
|
||||
|
||||
class ChatHistoryToolWindow(private val project: Project) : BorderLayoutPanel() {
|
||||
|
||||
companion object {
|
||||
private val KEY: Key<Boolean> = Key.create("SELECTED_STATE")
|
||||
private const val SEARCH_DEBOUNCE_MS = 300
|
||||
private const val MAX_MESSAGES_TO_SEARCH = 5
|
||||
}
|
||||
|
||||
private val conversationService = ConversationService.getInstance()
|
||||
private val chatHistoryListPanel = ChatHistoryListPanel()
|
||||
private val searchField = SearchTextField()
|
||||
private var allConversations = mutableListOf<Conversation>()
|
||||
private var sortOption = SortOption.UPDATED_DATE_DESC
|
||||
private val statusLabel = JBLabel().apply {
|
||||
font = JBFont.small()
|
||||
foreground = UIUtil.getContextHelpForeground()
|
||||
border = JBUI.Borders.empty(2, 8)
|
||||
}
|
||||
private var lastSearchText = ""
|
||||
private var lastFilteredConversations: List<Conversation>? = null
|
||||
private var isDataLoaded = false
|
||||
|
||||
enum class SortOption(val propertyKey: String) {
|
||||
UPDATED_DATE_DESC("conversation.sortOption.recentlyUpdated"),
|
||||
UPDATED_DATE_ASC("conversation.sortOption.oldestFirst"),
|
||||
TITLE_ASC("conversation.sortOption.titleAscending"),
|
||||
TITLE_DESC("conversation.sortOption.titleDescending"),
|
||||
MESSAGE_COUNT_DESC("conversation.sortOption.mostMessages"),
|
||||
MESSAGE_COUNT_ASC("conversation.sortOption.leastMessages");
|
||||
|
||||
val displayName: String
|
||||
get() = CodeGPTBundle.get(propertyKey)
|
||||
}
|
||||
|
||||
init {
|
||||
setupUI()
|
||||
loadConversationsAsync()
|
||||
setupListeners()
|
||||
}
|
||||
|
||||
private fun setupUI() {
|
||||
chatHistoryListPanel.apply {
|
||||
setOnConversationSelected { conversation ->
|
||||
ConversationsState.getInstance().setCurrentConversation(conversation)
|
||||
}
|
||||
|
||||
setOnConversationDoubleClicked { conversation ->
|
||||
project.getService(ChatToolWindowContentManager::class.java)
|
||||
.displayConversation(conversation)
|
||||
}
|
||||
|
||||
setOnConversationDeleted { conversation ->
|
||||
deleteConversation(conversation)
|
||||
}
|
||||
}
|
||||
|
||||
val searchPanel = createSearchPanel()
|
||||
|
||||
val topPanel = panel {
|
||||
row {
|
||||
cell(searchPanel)
|
||||
.align(AlignX.FILL)
|
||||
}.resizableRow()
|
||||
row {
|
||||
cell(statusLabel)
|
||||
.align(AlignX.FILL)
|
||||
.applyToComponent {
|
||||
border = JBUI.Borders.empty(2, 8)
|
||||
}
|
||||
}
|
||||
separator()
|
||||
}.apply {
|
||||
background = UIUtil.getPanelBackground()
|
||||
}
|
||||
|
||||
addToTop(topPanel)
|
||||
addToCenter(chatHistoryListPanel)
|
||||
}
|
||||
|
||||
private fun createSearchPanel(): JPanel {
|
||||
searchField.apply {
|
||||
textEditor.emptyText.text = CodeGPTBundle.get("conversation.searchField.placeholder")
|
||||
textEditor.border = JBUI.Borders.empty(2)
|
||||
textEditor.background = UIUtil.getPanelBackground().darker()
|
||||
}
|
||||
|
||||
return panel {
|
||||
row {
|
||||
cell(searchField)
|
||||
.align(AlignX.FILL)
|
||||
.resizableColumn()
|
||||
}
|
||||
}.apply {
|
||||
border = JBUI.Borders.empty(4, 8, 4, 8)
|
||||
background = UIUtil.getPanelBackground()
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateSearchFieldState() {
|
||||
SwingUtilities.invokeLater {
|
||||
val hasText = searchField.text.isNotBlank()
|
||||
if (hasText && allConversations.isNotEmpty()) {
|
||||
val matchCount = countMatchingConversations(searchField.text)
|
||||
searchField.textEditor.emptyText.text = formatSearchResultMessage(matchCount)
|
||||
} else {
|
||||
searchField.textEditor.emptyText.text =
|
||||
CodeGPTBundle.get("conversation.searchField.placeholder")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun countMatchingConversations(searchText: String): Int {
|
||||
return allConversations.count { conversation ->
|
||||
matchesSearchText(conversation, searchText)
|
||||
}
|
||||
}
|
||||
|
||||
private fun formatSearchResultMessage(count: Int): String {
|
||||
return if (count == 1) {
|
||||
CodeGPTBundle.get("conversation.searchResult.singular", count)
|
||||
} else {
|
||||
CodeGPTBundle.get("conversation.searchResult.plural", count)
|
||||
}
|
||||
}
|
||||
|
||||
private fun filterConversations(searchText: String): List<Conversation> {
|
||||
return if (searchText.isBlank()) {
|
||||
lastSearchText = ""
|
||||
lastFilteredConversations = null
|
||||
allConversations
|
||||
} else if (searchText == lastSearchText && lastFilteredConversations != null) {
|
||||
lastFilteredConversations!!
|
||||
} else {
|
||||
val startList = getOptimizedSearchStartList(searchText)
|
||||
val filtered = startList.filter { conversation ->
|
||||
matchesSearchText(conversation, searchText)
|
||||
}
|
||||
cacheFilterResults(searchText, filtered)
|
||||
filtered
|
||||
}
|
||||
}
|
||||
|
||||
private fun getOptimizedSearchStartList(searchText: String): List<Conversation> {
|
||||
return if (lastSearchText.isNotEmpty() && searchText.startsWith(lastSearchText) && lastFilteredConversations != null) {
|
||||
lastFilteredConversations!!
|
||||
} else {
|
||||
allConversations
|
||||
}
|
||||
}
|
||||
|
||||
private fun cacheFilterResults(searchText: String, filtered: List<Conversation>) {
|
||||
lastSearchText = searchText
|
||||
lastFilteredConversations = filtered
|
||||
}
|
||||
|
||||
private fun getSortIcon(sortOption: SortOption) = when (sortOption) {
|
||||
SortOption.UPDATED_DATE_DESC -> AllIcons.ObjectBrowser.SortByType
|
||||
SortOption.UPDATED_DATE_ASC -> AllIcons.ObjectBrowser.SortByType
|
||||
SortOption.TITLE_ASC -> AllIcons.ObjectBrowser.Sorted
|
||||
SortOption.TITLE_DESC -> AllIcons.ObjectBrowser.Sorted
|
||||
SortOption.MESSAGE_COUNT_DESC -> AllIcons.Actions.ListFiles
|
||||
SortOption.MESSAGE_COUNT_ASC -> AllIcons.Actions.ListFiles
|
||||
}
|
||||
|
||||
private fun createSortAction(): AnAction {
|
||||
val sortAction = object : AnAction(
|
||||
CodeGPTBundle.get("conversation.sortAction.title", sortOption.displayName),
|
||||
CodeGPTBundle.get("conversation.sortAction.description", sortOption.displayName),
|
||||
AllIcons.ObjectBrowser.Sorted
|
||||
) {
|
||||
override fun actionPerformed(e: AnActionEvent) {
|
||||
val actionGroup = DefaultActionGroup().apply {
|
||||
SortOption.entries.forEach { option ->
|
||||
add(object : AnAction(option.displayName, null, getSortIcon(option)) {
|
||||
override fun actionPerformed(e: AnActionEvent) {
|
||||
sortOption = option
|
||||
sortAndFilterConversations()
|
||||
}
|
||||
|
||||
override fun update(e: AnActionEvent) {
|
||||
e.presentation.putClientProperty(KEY, sortOption == option)
|
||||
}
|
||||
|
||||
override fun getActionUpdateThread(): ActionUpdateThread {
|
||||
return ActionUpdateThread.BGT
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
val popup = JBPopupFactory.getInstance()
|
||||
.createActionGroupPopup(
|
||||
CodeGPTBundle.get("conversation.sortPopup.title"),
|
||||
actionGroup,
|
||||
e.dataContext,
|
||||
JBPopupFactory.ActionSelectionAid.SPEEDSEARCH,
|
||||
true
|
||||
)
|
||||
|
||||
val component = e.inputEvent?.component
|
||||
if (component != null) {
|
||||
popup.showUnderneathOf(component)
|
||||
} else {
|
||||
popup.showInBestPositionFor(e.dataContext)
|
||||
}
|
||||
}
|
||||
|
||||
override fun update(e: AnActionEvent) {
|
||||
e.presentation.text =
|
||||
CodeGPTBundle.get("conversation.sortAction.title", sortOption.displayName)
|
||||
}
|
||||
|
||||
override fun getActionUpdateThread(): ActionUpdateThread {
|
||||
return ActionUpdateThread.BGT
|
||||
}
|
||||
}
|
||||
return sortAction
|
||||
}
|
||||
|
||||
private fun setupListeners() {
|
||||
var searchTimer: Timer? = null
|
||||
searchField.addDocumentListener(object : DocumentAdapter() {
|
||||
override fun textChanged(e: DocumentEvent) {
|
||||
updateSearchFieldState()
|
||||
searchTimer?.stop()
|
||||
searchTimer = Timer(SEARCH_DEBOUNCE_MS) {
|
||||
SwingUtilities.invokeLater {
|
||||
sortAndFilterConversations()
|
||||
}
|
||||
}
|
||||
searchTimer.isRepeats = false
|
||||
searchTimer.start()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fun getContent(): JPanel {
|
||||
val panel = SimpleToolWindowPanel(true)
|
||||
panel.setContent(this)
|
||||
val actionGroup = DefaultActionGroup("TOOLBAR_ACTION_GROUP", false).apply {
|
||||
add(object : AnAction(
|
||||
CodeGPTBundle.get("conversation.refreshAction.title"),
|
||||
CodeGPTBundle.get("conversation.refreshAction.description"),
|
||||
AllIcons.Actions.Refresh
|
||||
) {
|
||||
override fun actionPerformed(e: AnActionEvent) {
|
||||
refresh()
|
||||
}
|
||||
|
||||
override fun getActionUpdateThread(): ActionUpdateThread {
|
||||
return ActionUpdateThread.BGT
|
||||
}
|
||||
})
|
||||
add(createSortAction())
|
||||
addSeparator()
|
||||
add(DeleteAllConversationsAction { refresh() })
|
||||
}
|
||||
|
||||
val toolbar = ActionManager.getInstance()
|
||||
.createActionToolbar("NAVIGATION_BAR_TOOLBAR", actionGroup, true)
|
||||
toolbar.targetComponent = panel
|
||||
panel.toolbar = toolbar.component
|
||||
|
||||
return panel
|
||||
}
|
||||
|
||||
fun refresh() {
|
||||
loadConversationsAsync()
|
||||
}
|
||||
|
||||
private fun loadConversationsAsync() {
|
||||
thread {
|
||||
val conversations = conversationService.sortedConversations
|
||||
.filter { it.messages.isNotEmpty() }
|
||||
.filter { !(it.messages.size == 1 && it.messages[0].response.isNullOrBlank()) }
|
||||
.toMutableList()
|
||||
SwingUtilities.invokeLater {
|
||||
allConversations = conversations
|
||||
lastSearchText = ""
|
||||
lastFilteredConversations = null
|
||||
isDataLoaded = true
|
||||
sortAndFilterConversations()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun sortAndFilterConversations() {
|
||||
val searchText = searchField.text
|
||||
val filteredConversations = filterConversations(searchText)
|
||||
val sortedConversations = applySorting(filteredConversations)
|
||||
updateList(sortedConversations)
|
||||
}
|
||||
|
||||
private fun applySorting(conversations: List<Conversation>): List<Conversation> {
|
||||
return when (sortOption) {
|
||||
SortOption.UPDATED_DATE_DESC -> conversations.sortedByDescending { it.updatedOn }
|
||||
SortOption.UPDATED_DATE_ASC -> conversations.sortedBy { it.updatedOn }
|
||||
SortOption.TITLE_ASC -> conversations.sortedBy { getConversationDisplayTitle(it).lowercase() }
|
||||
SortOption.TITLE_DESC -> conversations.sortedByDescending {
|
||||
getConversationDisplayTitle(
|
||||
it
|
||||
).lowercase()
|
||||
}
|
||||
|
||||
SortOption.MESSAGE_COUNT_DESC -> conversations.sortedByDescending { it.messages.size }
|
||||
SortOption.MESSAGE_COUNT_ASC -> conversations.sortedBy { it.messages.size }
|
||||
}
|
||||
}
|
||||
|
||||
private fun getConversationDisplayTitle(conversation: Conversation): String {
|
||||
return conversation.title?.takeIf { it.isNotBlank() }
|
||||
?: conversation.messages.firstOrNull()?.prompt?.take(50)
|
||||
?: CodeGPTBundle.get("conversation.defaultTitle")
|
||||
}
|
||||
|
||||
private fun matchesSearchText(conversation: Conversation, searchText: String): Boolean {
|
||||
val searchLower = searchText.lowercase()
|
||||
return conversation.title?.lowercase()?.contains(searchLower) == true ||
|
||||
conversation.messages.take(MAX_MESSAGES_TO_SEARCH).any { message ->
|
||||
message.prompt?.lowercase()?.contains(searchLower) == true ||
|
||||
message.response?.lowercase()?.contains(searchLower) == true
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateList(conversations: List<Conversation>) {
|
||||
SwingUtilities.invokeLater {
|
||||
chatHistoryListPanel.setConversations(conversations)
|
||||
statusLabel.text = createStatusMessage(conversations)
|
||||
updateSelectedConversation(conversations)
|
||||
}
|
||||
}
|
||||
|
||||
private fun createStatusMessage(conversations: List<Conversation>): String {
|
||||
val searchText = searchField.text
|
||||
return buildString {
|
||||
append(getConversationCountMessage(conversations, searchText))
|
||||
append(" • ")
|
||||
append(CodeGPTBundle.get("conversation.status.sortedBy", sortOption.displayName))
|
||||
}
|
||||
}
|
||||
|
||||
private fun getConversationCountMessage(
|
||||
conversations: List<Conversation>,
|
||||
searchText: String
|
||||
): String {
|
||||
return if (searchText.isNotBlank()) {
|
||||
CodeGPTBundle.get(
|
||||
"conversation.status.searchResult",
|
||||
conversations.size,
|
||||
allConversations.size
|
||||
)
|
||||
} else {
|
||||
if (conversations.size == 1) {
|
||||
CodeGPTBundle.get("conversation.status.count.singular", conversations.size)
|
||||
} else {
|
||||
CodeGPTBundle.get("conversation.status.count.plural", conversations.size)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateSelectedConversation(conversations: List<Conversation>) {
|
||||
ConversationsState.getCurrentConversation()?.let { current ->
|
||||
conversations.find { it.id == current.id }?.let { conversation ->
|
||||
chatHistoryListPanel.setSelectedConversation(conversation)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun deleteConversation(conversation: Conversation) {
|
||||
val result = JOptionPane.showConfirmDialog(
|
||||
this,
|
||||
CodeGPTBundle.get("conversation.deleteConfirmation.message"),
|
||||
CodeGPTBundle.get("conversation.deleteConfirmation.title"),
|
||||
JOptionPane.YES_NO_OPTION,
|
||||
JOptionPane.WARNING_MESSAGE
|
||||
)
|
||||
|
||||
if (result == JOptionPane.YES_OPTION) {
|
||||
conversationService.deleteConversation(conversation)
|
||||
refresh()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -370,3 +370,28 @@ settings.models.chat.section.title=Chat
|
|||
settings.models.chat.section.description=Models for conversations, code edits, auto applies, commits, and naming suggestions. <a href="https://docs.tryproxy.io/editor/chat/overview">Learn more</a>
|
||||
settings.models.tab.section.title=Tab
|
||||
settings.models.tab.section.description=Models for autocomplete and multi-line next edit suggestions. <a href="https://docs.tryproxy.io/editor/tab/overview">Learn more</a>
|
||||
conversation.deleteButton.tooltip=Delete conversation
|
||||
conversation.messageCount.singular={0} message
|
||||
conversation.messageCount.plural={0} messages
|
||||
conversation.defaultTitle=New Conversation
|
||||
conversation.emptyState=Your conversations will be saved here for easy access.
|
||||
conversation.searchField.placeholder=Search in titles and messages...
|
||||
conversation.searchResult.singular={0} match found
|
||||
conversation.searchResult.plural={0} matches found
|
||||
conversation.sortOption.recentlyUpdated=Recently Updated
|
||||
conversation.sortOption.oldestFirst=Oldest First
|
||||
conversation.sortOption.titleAscending=Title (A-Z)
|
||||
conversation.sortOption.titleDescending=Title (Z-A)
|
||||
conversation.sortOption.mostMessages=Most Messages
|
||||
conversation.sortOption.leastMessages=Least Messages
|
||||
conversation.sortAction.title=Sort: {0}
|
||||
conversation.sortAction.description=Sort conversations by {0}
|
||||
conversation.sortPopup.title=Sort By
|
||||
conversation.refreshAction.title=Refresh
|
||||
conversation.refreshAction.description=Refresh conversation list
|
||||
conversation.status.searchResult=Found {0} of {1} conversations
|
||||
conversation.status.count.singular={0} conversation
|
||||
conversation.status.count.plural={0} conversations
|
||||
conversation.status.sortedBy=Sorted by: {0}
|
||||
conversation.deleteConfirmation.message=Are you sure you want to delete this conversation?
|
||||
conversation.deleteConfirmation.title=Delete Conversation
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue