From 25151ee42e53b8f1811955fbd68bc00362153e6f Mon Sep 17 00:00:00 2001 From: "carlrobertoh@gmail.com" Date: Mon, 10 Feb 2025 17:14:46 +0000 Subject: [PATCH] feat: add gemini 2.0 flash model and update model combobox menu ui --- .../chat/ui/textarea/ModelComboBoxAction.java | 31 +++-- .../service/codegpt/CodeGPTAvailableModels.kt | 16 +-- .../codegpt/toolwindow/ui/ModelListPopup.kt | 125 ++++++++++++++++++ 3 files changed, 155 insertions(+), 17 deletions(-) create mode 100644 src/main/kotlin/ee/carlrobert/codegpt/toolwindow/ui/ModelListPopup.kt diff --git a/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ui/textarea/ModelComboBoxAction.java b/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ui/textarea/ModelComboBoxAction.java index 7f8c7adf..ae5a6a07 100644 --- a/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ui/textarea/ModelComboBoxAction.java +++ b/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ui/textarea/ModelComboBoxAction.java @@ -23,6 +23,8 @@ import com.intellij.openapi.editor.colors.EditorColorsManager; import com.intellij.openapi.project.DumbAwareAction; import com.intellij.openapi.project.Project; import com.intellij.openapi.ui.popup.JBPopup; +import com.intellij.openapi.ui.popup.JBPopupListener; +import com.intellij.openapi.ui.popup.LightweightWindowEvent; import com.intellij.openapi.ui.popup.ListPopup; import ee.carlrobert.codegpt.CodeGPTKeys; import ee.carlrobert.codegpt.Icons; @@ -39,6 +41,8 @@ import ee.carlrobert.codegpt.settings.service.google.GoogleSettings; import ee.carlrobert.codegpt.settings.service.llama.LlamaSettings; import ee.carlrobert.codegpt.settings.service.ollama.OllamaSettings; import ee.carlrobert.codegpt.settings.service.openai.OpenAISettings; +import ee.carlrobert.codegpt.toolwindow.ui.CodeGPTModelsListPopupAction; +import ee.carlrobert.codegpt.toolwindow.ui.ModelListPopup; import ee.carlrobert.llm.client.google.models.GoogleModel; import ee.carlrobert.llm.client.openai.completion.OpenAIChatCompletionModel; import java.awt.Color; @@ -101,8 +105,16 @@ public class ModelComboBoxAction extends ComboBoxAction { @Override protected JBPopup createActionPopup(DefaultActionGroup group, @NotNull DataContext context, - @Nullable Runnable disposeCallback) { - ListPopup popup = (ListPopup) super.createActionPopup(group, context, disposeCallback); + @Nullable Runnable disposeCallback) { + ListPopup popup = new ModelListPopup(group, context); + if (disposeCallback != null) { + popup.addListener(new JBPopupListener() { + @Override + public void onClosed(@NotNull LightweightWindowEvent event) { + disposeCallback.run(); + } + }); + } popup.setShowSubmenuOnHover(true); return popup; } @@ -323,13 +335,14 @@ public class ModelComboBoxAction extends ComboBoxAction { } private AnAction createCodeGPTModelAction(CodeGPTModel model, Presentation comboBoxPresentation) { - - return createModelAction(CODEGPT, model.getName(), model.getIcon(), comboBoxPresentation, - () -> ApplicationManager.getApplication() - .getService(CodeGPTServiceSettings.class) - .getState() - .getChatCompletionSettings() - .setModel(model.getCode())); + return new CodeGPTModelsListPopupAction(model, comboBoxPresentation, () -> { + ApplicationManager.getApplication() + .getService(CodeGPTServiceSettings.class) + .getState() + .getChatCompletionSettings() + .setModel(model.getCode()); + handleModelChange(CODEGPT); + }); } private AnAction createOllamaModelAction(String model, Presentation comboBoxPresentation) { diff --git a/src/main/kotlin/ee/carlrobert/codegpt/settings/service/codegpt/CodeGPTAvailableModels.kt b/src/main/kotlin/ee/carlrobert/codegpt/settings/service/codegpt/CodeGPTAvailableModels.kt index 64b9af19..311bc65f 100644 --- a/src/main/kotlin/ee/carlrobert/codegpt/settings/service/codegpt/CodeGPTAvailableModels.kt +++ b/src/main/kotlin/ee/carlrobert/codegpt/settings/service/codegpt/CodeGPTAvailableModels.kt @@ -7,7 +7,8 @@ import javax.swing.Icon object CodeGPTAvailableModels { - val DEFAULT_CHAT_MODEL = CodeGPTModel("GPT-4o", "gpt-4o", Icons.OpenAI, INDIVIDUAL) + val DEFAULT_CHAT_MODEL = + CodeGPTModel("Gemini 2.0 Flash", "gemini-flash-2.0", Icons.Google, ANONYMOUS) val DEFAULT_CODE_MODEL = CodeGPTModel("Codestral", "codestral", Icons.Mistral, ANONYMOUS) @JvmStatic @@ -18,9 +19,8 @@ object CodeGPTAvailableModels { CodeGPTModel("GPT-4o", "gpt-4o", Icons.OpenAI, INDIVIDUAL), CodeGPTModel("Claude 3.5 Sonnet", "claude-3.5-sonnet", Icons.Anthropic, INDIVIDUAL), CodeGPTModel("DeepSeek R1", "deepseek-r1", Icons.DeepSeek, INDIVIDUAL), - CodeGPTModel("Gemini 1.5 Pro", "gemini-pro-1.5", Icons.Google, INDIVIDUAL), - CodeGPTModel("DeepSeek Coder V2 - FREE", "deepseek-coder-v2", Icons.DeepSeek, ANONYMOUS), - CodeGPTModel("GPT-4o mini - FREE", "gpt-4o-mini", Icons.OpenAI, ANONYMOUS), + CodeGPTModel("Gemini 2.0 Flash", "gemini-flash-2.0", Icons.Google, ANONYMOUS), + CodeGPTModel("GPT-4o mini", "gpt-4o-mini", Icons.OpenAI, ANONYMOUS), ) FREE -> listOf( @@ -31,7 +31,7 @@ object CodeGPTAvailableModels { CodeGPTModel("DeepSeek V3", "deepseek-v3", Icons.DeepSeek, FREE), CodeGPTModel("Qwen 2.5 Coder (32B)", "qwen-2.5-32b-chat", Icons.Qwen, FREE), CodeGPTModel("Llama 3.1 (405B)", "llama-3.1-405b", Icons.Meta, FREE), - CodeGPTModel("DeepSeek Coder V2", "deepseek-coder-v2", Icons.DeepSeek, ANONYMOUS), + CodeGPTModel("Gemini 2.0 Flash", "gemini-flash-2.0", Icons.Google, ANONYMOUS), ) INDIVIDUAL -> listOf( @@ -40,7 +40,7 @@ object CodeGPTAvailableModels { CodeGPTModel("Claude 3.5 Sonnet", "claude-3.5-sonnet", Icons.Anthropic, INDIVIDUAL), CodeGPTModel("DeepSeek R1", "deepseek-r1", Icons.DeepSeek, INDIVIDUAL), CodeGPTModel("DeepSeek V3", "deepseek-v3", Icons.DeepSeek, FREE), - CodeGPTModel("Gemini 1.5 Pro", "gemini-pro-1.5", Icons.Google, INDIVIDUAL), + CodeGPTModel("Gemini 2.0 Flash", "gemini-flash-2.0", Icons.Google, ANONYMOUS), ) } } @@ -52,11 +52,11 @@ object CodeGPTAvailableModels { CodeGPTModel("GPT-4o mini", "gpt-4o-mini", Icons.OpenAI, ANONYMOUS), CodeGPTModel("Claude 3.5 Sonnet", "claude-3.5-sonnet", Icons.Anthropic, INDIVIDUAL), CodeGPTModel("Gemini 1.5 Pro", "gemini-pro-1.5", Icons.Google, INDIVIDUAL), + CodeGPTModel("Gemini 2.0 Flash", "gemini-flash-2.0", Icons.Google, ANONYMOUS), CodeGPTModel("Qwen 2.5 Coder (32B)", "qwen-2.5-32b-chat", Icons.Qwen, FREE), CodeGPTModel("Llama 3.1 (405B)", "llama-3.1-405b", Icons.Meta, FREE), CodeGPTModel("DeepSeek R1", "deepseek-r1", Icons.DeepSeek, INDIVIDUAL), CodeGPTModel("DeepSeek V3", "deepseek-v3", Icons.DeepSeek, FREE), - CodeGPTModel("DeepSeek Coder V2", "deepseek-coder-v2", Icons.DeepSeek, FREE), ) @JvmStatic @@ -76,5 +76,5 @@ data class CodeGPTModel( val name: String, val code: String, val icon: Icon, - val individual: PricingPlan + val pricingPlan: PricingPlan ) diff --git a/src/main/kotlin/ee/carlrobert/codegpt/toolwindow/ui/ModelListPopup.kt b/src/main/kotlin/ee/carlrobert/codegpt/toolwindow/ui/ModelListPopup.kt new file mode 100644 index 00000000..9569d528 --- /dev/null +++ b/src/main/kotlin/ee/carlrobert/codegpt/toolwindow/ui/ModelListPopup.kt @@ -0,0 +1,125 @@ +package ee.carlrobert.codegpt.toolwindow.ui + +import com.intellij.openapi.actionSystem.* +import com.intellij.openapi.actionSystem.impl.MenuItemPresentationFactory +import com.intellij.openapi.project.DumbAwareAction +import com.intellij.ui.SimpleColoredComponent +import com.intellij.ui.SimpleTextAttributes +import com.intellij.ui.popup.PopupFactoryImpl +import com.intellij.ui.popup.PopupFactoryImpl.ActionItem +import com.intellij.ui.popup.list.PopupListElementRenderer +import com.intellij.util.ui.JBUI +import ee.carlrobert.codegpt.CodeGPTKeys.CODEGPT_USER_DETAILS +import ee.carlrobert.codegpt.settings.service.codegpt.CodeGPTModel +import ee.carlrobert.llm.client.codegpt.PricingPlan.ANONYMOUS +import ee.carlrobert.llm.client.codegpt.PricingPlan.INDIVIDUAL +import java.awt.BorderLayout +import javax.swing.* + +class ModelListPopup( + actionGroup: ActionGroup, + context: DataContext +) : PopupFactoryImpl.ActionGroupPopup( + null, + actionGroup, + context, + false, + false, + true, + false, + null, + -1, + null, + null, + MenuItemPresentationFactory(), + false +) { + + override fun getListElementRenderer(): ListCellRenderer<*> { + return object : PopupListElementRenderer(this) { + private lateinit var secondaryLabel: SimpleColoredComponent + + override fun createLabel() { + super.createLabel() + secondaryLabel = SimpleColoredComponent() + } + + override fun createItemComponent(): JComponent? { + createLabel() + val panel = JPanel(BorderLayout()).apply { + add(myTextLabel, BorderLayout.WEST) + add(secondaryLabel, BorderLayout.EAST) + } + myIconBar = createIconBar() + return layoutComponent(panel) + } + + override fun createIconBar(): JComponent? { + return Box.createHorizontalBox().apply { + border = JBUI.Borders.emptyRight(JBUI.CurrentTheme.ActionsList.elementIconGap()) + add(myIconLabel) + } + } + + override fun customizeComponent( + list: JList?, + value: Any?, + isSelected: Boolean + ) { + super.customizeComponent(list, value, isSelected) + setupSecondaryLabel() + + (value as? ActionItem)?.action?.let { action -> + if (action is CodeGPTModelsListPopupAction) { + updateSecondaryLabel(action) + } + } + } + + private fun setupSecondaryLabel() { + secondaryLabel.apply { + font = JBUI.Fonts.toolbarSmallComboBoxFont() + border = JBUI.Borders.emptyLeft(8) + clear() + } + } + + private fun updateSecondaryLabel(action: CodeGPTModelsListPopupAction) { + val userPricingPlan = CODEGPT_USER_DETAILS.get(project)?.pricingPlan + if (userPricingPlan != INDIVIDUAL && action.model.pricingPlan == INDIVIDUAL) { + secondaryLabel.append("PRO", SimpleTextAttributes.GRAYED_BOLD_ATTRIBUTES, true) + } + } + } + } +} + +class CodeGPTModelsListPopupAction( + val model: CodeGPTModel, + private val comboBoxPresentation: Presentation, + private val onModelChanged: Runnable? +) : DumbAwareAction(model.name, "", model.icon) { + + override fun update(event: AnActionEvent) { + event.presentation.isEnabled = shouldEnableAction(event) + } + + private fun shouldEnableAction(event: AnActionEvent): Boolean { + val project = event.project ?: return false + val notSelected = event.presentation.text != comboBoxPresentation.text + val pricingPlan = CODEGPT_USER_DETAILS[project]?.pricingPlan + + if (pricingPlan == null || pricingPlan == ANONYMOUS) { + return notSelected && model.pricingPlan == ANONYMOUS + } + return notSelected + } + + override fun actionPerformed(e: AnActionEvent) { + onModelChanged?.run() + } + + override fun getActionUpdateThread(): ActionUpdateThread { + return ActionUpdateThread.BGT + } +}