Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[REFACTORINGS] Suggested by Google Gemini 2.0 Flash & Gemini Exp 1206 #371

Open
stephanj opened this issue Dec 11, 2024 · 3 comments
Open
Labels
documentation Improvements or additions to documentation

Comments

@stephanj
Copy link
Contributor

Google Gemini 2.0 Flash Exp Suggestions

Okay, let's analyze the provided code structure and identify potential refactorings to improve readability, focusing on the com.devoxx.genie package.

General Observations

  • Large Number of Utility Classes: There are many utility classes under com.devoxx.genie.ui.util. While utilities are useful, having too many can make it harder to understand the overall flow.
  • Deeply Nested Packages: The package structure is quite deep, especially under com.devoxx.genie.ui. This can make it harder to navigate and understand the purpose of each class.
  • Inconsistent Naming: Some class names are very descriptive (e.g., WindowContextFormatterUtil), while others are more generic (e.g., HelpUtil).
  • Mix of UI and Logic: Some UI-related classes (e.g., ActionButtonsPanel) seem to contain a lot of logic, which could be separated for better maintainability.
  • Settings Components: The settings components are well-organized but could benefit from a more consistent naming scheme for the configurable and component classes.
  • Lack of Interfaces: There are not many interfaces used, which could make the code less flexible and harder to test.

Refactoring Suggestions

Here's a breakdown of refactoring suggestions, categorized by area:

  1. com.devoxx.genie.ui.util Package

    • Group Related Utilities: Consider grouping related utilities into sub-packages or fewer, more focused classes. For example, UI-related utilities could go into a ui sub-package, and string formatting utilities into a format sub-package.
    • Rename Generic Utilities: Rename classes like HelpUtil to be more specific to their purpose (e.g., CustomPromptHelpUtil).
    • Consider Static Methods: Many of these utilities seem to be stateless. Consider making methods static where appropriate.
    • Introduce Interfaces: If some utilities have multiple implementations, consider introducing interfaces to allow for more flexibility.
    --- a/src/main/java/com/devoxx/genie/ui/util/HelpUtil.java
    +++ b/src/main/java/com/devoxx/genie/ui/util/HelpUtil.java
    @@ -4,7 +4,7 @@
    

public class HelpUtil {

  • private HelpUtil() {
  • private HelpUtil() { // Consider making this class abstract if it only contains static methods
    }

    public static @NotNull String getHelpMessage(@NotNull ResourceBundle resourceBundle) {
    ```

  1. com.devoxx.genie.ui.panel Package

    • Separate UI from Logic: Move logic out of UI classes like ActionButtonsPanel into dedicated controller classes.
    • Use a Consistent Naming Scheme: Ensure all panel classes follow a consistent naming scheme (e.g., XyzPanel or XyzView).
    • Consider Abstract Base Classes: If multiple panels share common functionality, consider creating an abstract base class.
    • Simplify Panel Creation: Use helper methods or factories to simplify the creation of panels.
    --- a/src/main/java/com/devoxx/genie/ui/panel/ActionButtonsPanel.java
    +++ b/src/main/java/com/devoxx/genie/ui/panel/ActionButtonsPanel.java
    @@ -1,6 +1,7 @@
     package com.devoxx.genie.ui.panel;
    
     import com.devoxx.genie.controller.ActionPanelController;
  • import com.devoxx.genie.controller.PromptSubmissionController;
    import com.devoxx.genie.model.Constant;
    import com.devoxx.genie.model.LanguageModel;
    import com.devoxx.genie.model.enumarations.ModelProvider;
    ```
  1. com.devoxx.genie.ui.settings Package

    • Consistent Naming: Use a consistent naming scheme for configurable and component classes (e.g., XyzSettingsConfigurable and XyzSettingsComponent).
    • Abstract Base Classes: Use abstract base classes for common functionality in settings components.
    • Separate UI from Logic: Move logic out of settings components into dedicated handler classes.
    --- a/src/main/java/com/devoxx/genie/ui/settings/llm/LLMProvidersComponent.java
    +++ b/src/main/java/com/devoxx/genie/ui/settings/llm/LLMProvidersComponent.java
    @@ -1,6 +1,7 @@
     package com.devoxx.genie.ui.settings.llm;
    
     import com.devoxx.genie.service.PropertiesService;
  • import com.devoxx.genie.ui.listener.LLMSettingsChangeListener;
    import com.devoxx.genie.ui.settings.AbstractSettingsComponent;
    import com.intellij.util.ui.JBUI;
    import lombok.Getter;
    ```
  1. com.devoxx.genie.service Package

    • Interface-Based Design: Use interfaces for services to allow for more flexibility and testability.
    • Separate Concerns: Ensure each service has a clear responsibility and doesn't mix concerns.
    • Use Dependency Injection: Consider using dependency injection to manage dependencies between services.
    --- a/src/main/java/com/devoxx/genie/service/ChatService.java
    +++ b/src/main/java/com/devoxx/genie/service/ChatService.java
    @@ -1,6 +1,7 @@
     package com.devoxx.genie.service;
    
     import com.devoxx.genie.model.conversation.Conversation;
  • import com.devoxx.genie.model.enumarations.ModelProvider;
    import com.devoxx.genie.model.request.ChatMessageContext;
    import com.devoxx.genie.service.FileListManager;
    import com.devoxx.genie.ui.ConversationStarter;
    ```
  1. com.devoxx.genie.model Package

    • Use Records: Use records for data classes where appropriate.
    • Enums for Constants: Use enums for constants where appropriate.
    --- a/src/main/java/com/devoxx/genie/model/request/SemanticFile.java
    +++ b/src/main/java/com/devoxx/genie/model/request/SemanticFile.java
    @@ -1,3 +1,4 @@

package com.devoxx.genie.model.request;

public record SemanticFile(String filePath, double score) {
```

  1. com.devoxx.genie.controller Package

    • Introduce Controllers: Create controller classes to handle the logic for UI components.
    • Clear Responsibilities: Ensure each controller has a clear responsibility and doesn't mix concerns.
    --- a/src/main/java/com/devoxx/genie/controller/ActionPanelController.java
    +++ b/src/main/java/com/devoxx/genie/controller/ActionPanelController.java
    @@ -1,6 +1,7 @@
     package com.devoxx.genie.controller;
    
  • import com.devoxx.genie.chatmodel.ChatModelFactoryProvider;
  • import com.devoxx.genie.chatmodel.ChatModelProvider;
    import com.devoxx.genie.model.Constant;
    import com.devoxx.genie.model.LanguageModel;
    import com.devoxx.genie.model.enumarations.ModelProvider;
    ```

Example Refactoring: ActionButtonsPanel

Here's a simplified example of how you might refactor ActionButtonsPanel:

  1. Create a Controller: Create a new class ActionPanelController to handle the logic.
  2. Move Logic: Move the logic for handling button clicks and prompt execution to the controller.
  3. Update ActionButtonsPanel: Update ActionButtonsPanel to delegate actions to the controller.
// New controller class
package com.devoxx.genie.controller;

import com.devoxx.genie.model.Constant;
import com.devoxx.genie.model.LanguageModel;
import com.devoxx.genie.model.enumarations.ModelProvider;
import com.devoxx.genie.service.ChatPromptExecutor;
import com.devoxx.genie.ui.component.input.PromptInputArea;
import com.devoxx.genie.ui.panel.ActionButtonsPanel;
import com.devoxx.genie.ui.panel.PromptOutputPanel;
import com.devoxx.genie.ui.settings.DevoxxGenieStateService;
import com.devoxx.genie.ui.util.NotificationUtil;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.ui.ComboBox;
import org.jetbrains.annotations.NotNull;

public class ActionPanelController {
    private final Project project;
    private final PromptInputArea promptInputArea;
    private final PromptOutputPanel promptOutputPanel;
    private final ComboBox<ModelProvider> modelProviderComboBox;
    private final ComboBox<LanguageModel> modelNameComboBox;
    private final ChatPromptExecutor chatPromptExecutor;
    private final ActionButtonsPanel actionButtonsPanel;
    private boolean isPromptRunning = false;

    public ActionPanelController(Project project,
                                 PromptInputArea promptInputArea,
                                 PromptOutputPanel promptOutputPanel,
                                 ComboBox<ModelProvider> modelProviderComboBox,
                                 ComboBox<LanguageModel> modelNameComboBox,
                                 ActionButtonsPanel actionButtonsPanel) {
        this.project = project;
        this.promptInputArea = promptInputArea;
        this.promptOutputPanel = promptOutputPanel;
        this.modelProviderComboBox = modelProviderComboBox;
        this.modelNameComboBox = modelNameComboBox;
        this.chatPromptExecutor = new ChatPromptExecutor(promptInputArea);
        this.actionButtonsPanel = actionButtonsPanel;
    }

    public boolean isPromptRunning() {
        return isPromptRunning;
    }

    public boolean executePrompt(String actionCommand,
                                 boolean isProjectContextAdded,
                                 String projectContext) {
        if (isPromptRunning) {
            stopPromptExecution();
            return true;
        }

        if (!validateAndPreparePrompt(actionCommand, isProjectContextAdded, projectContext)) {
            return false;
        }

        isPromptRunning = true;

        chatPromptExecutor.updatePromptWithCommandIfPresent(currentChatMessageContext, promptOutputPanel)
            .ifPresentOrElse(
                this::executePromptWithContext,
                () -> isPromptRunning = false
            );

        return isPromptRunning;
    }

    private void executePromptWithContext(String command) {
        chatPromptExecutor.executePrompt(currentChatMessageContext, promptOutputPanel, () -> {
            isPromptRunning = false;
            actionButtonsPanel.enableButtons();
            ApplicationManager.getApplication().invokeLater(() -> {
                promptInputArea.clear();
                promptInputArea.requestInputFocus();
            });
        });
    }

    public void stopPromptExecution() {
        chatPromptExecutor.stopPromptExecution(project);
        isPromptRunning = false;
        actionButtonsPanel.enableButtons();
    }

    private ChatMessageContext currentChatMessageContext;

    private boolean validateAndPreparePrompt(String actionCommand,
                                             boolean isProjectContextAdded,
                                             String projectContext) {
        String userPromptText = getUserPromptText();
        if (userPromptText == null) {
            return false;
        }

        DevoxxGenieStateService stateService = DevoxxGenieStateService.getInstance();
        LanguageModel selectedLanguageModel = (LanguageModel) modelNameComboBox.getSelectedItem();

        // If selectedLanguageModel is null, create a default one
        if (selectedLanguageModel == null) {
            selectedLanguageModel = createDefaultLanguageModel(stateService);
        }

        currentChatMessageContext = ChatMessageContextUtil.createContext(
            project,
            userPromptText,
            selectedLanguageModel,
            new ChatModelProvider(),
            stateService,
            actionCommand,
            editorFileButtonManager,
            projectContext,
            isProjectContextAdded
        );

        return true;
    }

    private String getUserPromptText() {
        String userPromptText = promptInputArea.getText();
        if (userPromptText.isEmpty()) {
            NotificationUtil.sendNotification(project, "Please enter a prompt.");
            return null;
        }
        return userPromptText;
    }

    private LanguageModel createDefaultLanguageModel(@NotNull DevoxxGenieSettingsService stateService) {
        ModelProvider selectedProvider = (ModelProvider) modelProviderComboBox.getSelectedItem();
        if (selectedProvider != null &&
            (selectedProvider.equals(ModelProvider.LMStudio) ||
                selectedProvider.equals(ModelProvider.GPT4All) ||
                selectedProvider.equals(ModelProvider.Jlama) ||
                selectedProvider.equals(ModelProvider.LLaMA))) {
            return LanguageModel.builder()
                .provider(selectedProvider)
                .apiKeyUsed(false)
                .inputCost(0)
                .outputCost(0)
                .contextWindow(4096)
                .build();
        } else {
            String modelName = stateService.getSelectedLanguageModel(project.getLocationHash());
            return LanguageModel.builder()
                .provider(selectedProvider != null ? selectedProvider : ModelProvider.OpenAI)
                .modelName(modelName)
                .apiKeyUsed(false)
                .inputCost(0)
                .outputCost(0)
                .contextWindow(128_000)
                .build();
        }
    }
}
// Updated ActionButtonsPanel
package com.devoxx.genie.ui.panel;

import com.devoxx.genie.controller.ActionPanelController;
import com.devoxx.genie.model.Constant;
import com.devoxx.genie.model.LanguageModel;
import com.devoxx.genie.model.enumarations.ModelProvider;
import com.devoxx.genie.service.ProjectContentService;
import com.devoxx.genie.service.TokenCalculationService;
import com.devoxx.genie.ui.DevoxxGenieToolWindowContent;
import com.devoxx.genie.ui.EditorFileButtonManager;
import com.devoxx.genie.ui.component.ContextPopupMenu;
import com.devoxx.genie.ui.component.JHoverButton;
import com.devoxx.genie.ui.component.input.PromptInputArea;
import com.devoxx.genie.ui.component.TokenUsageBar;
import com.devoxx.genie.ui.listener.PromptSubmissionListener;
import com.devoxx.genie.ui.listener.SettingsChangeListener;
import com.devoxx.genie.ui.topic.AppTopics;
import com.devoxx.genie.ui.util.NotificationUtil;
import com.devoxx.genie.ui.util.WindowContextFormatterUtil;
import com.devoxx.genie.util.DefaultLLMSettingsUtil;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.ui.ComboBox;
import com.intellij.openapi.ui.Messages;
import com.intellij.openapi.ui.popup.JBPopup;
import com.intellij.openapi.ui.popup.JBPopupFactory;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.util.messages.MessageBusConnection;
import com.intellij.util.ui.JBUI;
import com.knuddels.jtokkit.Encodings;
import com.knuddels.jtokkit.api.EncodingType;
import org.jetbrains.annotations.NotNull;

import javax.swing.*;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;

import static com.devoxx.genie.model.Constant.*;
import static com.devoxx.genie.ui.util.DevoxxGenieIconsUtil.*;

public class ActionButtonsPanel extends JPanel implements SettingsChangeListener, PromptSubmissionListener {

    private final transient Project project;

    private final transient EditorFileButtonManager editorFileButtonManager;
    private final JPanel calcProjectPanel = new JPanel(new GridLayout(1, 2));

    private final JButton addFileBtn = new JHoverButton(AddFileIcon, false);
    private final JButton submitBtn = new JHoverButton(SubmitIcon, false);
    private final JButton addProjectBtn = new JHoverButton(ADD_PROJECT_TO_CONTEXT, AddFileIcon, true);
    private final JButton calcTokenCostBtn = new JHoverButton(CALC_TOKENS_COST, CalculateIcon, true);
    private final JPanel mainContent = new JPanel(new BorderLayout());

    private final PromptInputArea promptInputArea;
    private final ComboBox<ModelProvider> llmProvidersComboBox;
    private final ComboBox<LanguageModel> modelNameComboBox;
    private final TokenUsageBar tokenUsageBar = new TokenUsageBar();
    private int tokenCount;

    private final transient DevoxxGenieToolWindowContent devoxxGenieToolWindowContent;

    private boolean isProjectContextAdded = false;
    private String projectContext;

    private final transient TokenCalculationService tokenCalculationService;
    private final transient ActionPanelController controller;

    private final JPanel buttonPanel = new JPanel(new FlowLayout(FlowLayout.LEFT, 5, 0));
    private final SubmitPanel submitPanel;

    public ActionButtonsPanel(Project project,
                              SubmitPanel submitPanel,
                              PromptInputArea promptInputArea,
                              PromptOutputPanel promptOutputPanel,
                              ComboBox<ModelProvider> llmProvidersComboBox,
                              ComboBox<LanguageModel> modelNameComboBox,
                              DevoxxGenieToolWindowContent devoxxGenieToolWindowContent) {
        setLayout(new BorderLayout());
        setBorder(JBUI.Borders.empty(10));

        // Initialize fields and components
        this.project = project;
        this.submitPanel = submitPanel;
        this.promptInputArea = promptInputArea;
        this.editorFileButtonManager = new EditorFileButtonManager(project, addFileBtn);
        this.llmProvidersComboBox = llmProvidersComboBox;
        this.modelNameComboBox = modelNameComboBox;
        this.devoxxGenieToolWindowContent = devoxxGenieToolWindowContent;
        this.tokenCalculationService = new TokenCalculationService();

        this.controller = new ActionPanelController(
                project, promptInputArea, promptOutputPanel,
                llmProvidersComboBox, modelNameComboBox, this
        );

        this.llmProvidersComboBox.addActionListener(e -> updateButtonVisibility());

        setupUI();
        setupAccessibility();
        setupMessageBus();
    }

    private void setupUI() {

        // Setup token usage bar
        tokenUsageBar.setVisible(false);
        tokenUsageBar.setPreferredSize(new Dimension(Integer.MAX_VALUE, 3));
        JPanel progressPanel = new JPanel(new BorderLayout());
        progressPanel.add(tokenUsageBar, BorderLayout.CENTER);
        add(progressPanel, BorderLayout.NORTH);

        // Configure buttons
        setupButtons();

        // Add button panel to main content
        mainContent.add(buttonPanel, BorderLayout.CENTER);
        add(mainContent, BorderLayout.CENTER);
    }

    private void setupButtons() {
        // Configure Submit button
        submitBtn.setToolTipText(SUBMIT_THE_PROMPT + SHIFT_ENTER);
        submitBtn.setActionCommand(Constant.SUBMIT_ACTION);
        submitBtn.addActionListener(e -> onSubmitPrompt(e));

        // Configure Add File button
        addFileBtn.setToolTipText(ADD_FILE_S_TO_PROMPT_CONTEXT);
        addFileBtn.addActionListener(this::selectFilesForPromptContext);

        calcTokenCostBtn.setToolTipText(CALCULATE_TOKENS_COST);
        calcTokenCostBtn.addActionListener(e -> calculateTokensAndCost());

        addProjectBtn.setToolTipText(ADD_ENTIRE_PROJECT_TO_PROMPT_CONTEXT);
        addProjectBtn.addActionListener(e -> {
            if (isProjectContextAdded) {
                confirmProjectContextRemoval();
            } else {
                addProjectToContext();
            }
        });

        buttonPanel.setLayout(new GridLayout(1, 4, 5, 0));

        // Add buttons with horizontal glue between them
        buttonPanel.add(submitBtn);
        buttonPanel.add(calcTokenCostBtn);
        buttonPanel.add(addProjectBtn);
        buttonPanel.add(addFileBtn);

        // Set minimum size for buttons to prevent them from becoming too small
        Dimension minSize = new Dimension(100, 30);
        submitBtn.setMinimumSize(minSize);
        calcTokenCostBtn.setMinimumSize(minSize);
        addProjectBtn.setMinimumSize(minSize);
        addFileBtn.setMinimumSize(minSize);

        // Set maximum size to prevent buttons from growing too large
        Dimension maxSize = new Dimension(200, 30);
        submitBtn.setMaximumSize(maxSize);
        calcTokenCostBtn.setMaximumSize(maxSize);
        addProjectBtn.setMaximumSize(maxSize);
        addFileBtn.setMaximumSize(maxSize);
    }

    
    private void selectFilesForPromptContext(ActionEvent e) {
        java.util.List<VirtualFile> openFiles = editorFileButtonManager.getOpenFiles();
        List<VirtualFile> sortedFiles = new ArrayList<>(openFiles);
        sortedFiles.sort(Comparator.comparing(VirtualFile::getName, String.CASE_INSENSITIVE_ORDER));

        JBPopup popup = JBPopupFactory.getInstance()
            .createComponentPopupBuilder(FileSelectionPanelFactory.createPanel(project, sortedFiles), null)
            .setTitle(FILTER_AND_DOUBLE_CLICK_TO_ADD_TO_PROMPT_CONTEXT)
            .setRequestFocus(true)
            .setResizable(true)
            .setMovable(false)
            .setMinSize(new Dimension(300, 350))
            .createPopup();

        if (addFileBtn.isShowing()) {
            new ContextPopupMenu().show(submitBtn,
                popup,
                devoxxGenieToolWindowContent.getContentPanel().getSize().width,
                promptInputArea.getLocationOnScreen().y);
        }
    }

    
    private void onSubmitPrompt(ActionEvent actionEvent) {
        if (controller.isPromptRunning()) {
            controller.stopPromptExecution();
            return;
        }

        disableUIForPromptExecution();

        boolean response = controller.executePrompt(actionEvent.getActionCommand(), isProjectContextAdded, projectContext);
        if (!response) {
            enableButtons();
        }
    }

    private void disableUIForPromptExecution() {
        disableSubmitBtn();
        disableButtons();
        submitPanel.startGlowing();
    }

    public void enableButtons() {
        ApplicationManager.getApplication().invokeLater(() -> {
            submitBtn.setIcon(SubmitIcon);
            submitBtn.setToolTipText(SUBMIT_THE_PROMPT + " (Ctrl+Enter)");
            promptInputArea.setEnabled(true);
            submitPanel.stopGlowing();
        });
    }

    private void disableSubmitBtn() {
        ApplicationManager.getApplication().invokeLater(() -> {
            submitBtn.setIcon(StopIcon);
            submitBtn.setToolTipText(PROMPT_IS_RUNNING_PLEASE_BE_PATIENT);
        });
    }

    private void setupMessageBus() {
        MessageBusConnection messageBusConnection = ApplicationManager.getApplication().getMessageBus().connect();
        messageBusConnection.subscribe(AppTopics.SETTINGS_CHANGED_TOPIC, this);
        messageBusConnection.subscribe(AppTopics.PROMPT_SUBMISSION_TOPIC, this);
    }

    @Override
    public void setSize(@NotNull Dimension dimension) {
        super.setSize(dimension);
        revalidateLayout();
    }

    private void revalidateLayout() {
        if (getWidth() < 400) {
            buttonPanel.setLayout(new GridLayout(0, 1, 0, 5)); // Stack vertically when narrow
        } else {
            buttonPanel.setLayout(new GridBagLayout()); // Single row when wide
        }
        buttonPanel.revalidate();
    }

    private void disableButtons() {
        promptInputArea.setEnabled(false);
    }

    public void resetProjectContext() {
        updateAddProjectButton();
    }

    private void updateAddProjectButton() {
        if (isProjectContextAdded) {
            addProjectBtn.setIcon(DeleteIcon);
            addProjectBtn.setText(REMOVE_CONTEXT);
            addProjectBtn.setToolTipText(REMOVE_ENTIRE_PROJECT_FROM_PROMPT_CONTEXT);
        } else {
            addProjectBtn.setIcon(AddFileIcon);
            addProjectBtn.setText(ADD_PROJECT_TO_CONTEXT);
            addProjectBtn.setToolTipText(ADD_ENTIRE_PROJECT_TO_PROMPT_CONTEXT);
        }
    }

    
    private boolean isProjectContextSupportedProvider() {
        ModelProvider selectedProvider = (ModelProvider) llmProvidersComboBox.getSelectedItem();
        return selectedProvider != null && isSupportedProvider(selectedProvider);
    }

    private void removeProjectContext() {
        projectContext = null;
        isProjectContextAdded = false;

        addProjectBtn.setIcon(AddFileIcon);
        addProjectBtn.setText(ADD_PROJECT_TO_CONTEXT);
        addProjectBtn.setToolTipText(ADD_ENTIRE_PROJECT_TO_PROMPT_CONTEXT);

        resetTokenUsageBar();
        tokenCount = 0;

        NotificationUtil.sendNotification(project, "Project context removed successfully");
    }

    private boolean isSupportedProvider(@NotNull ModelProvider modelProvider) {
        return modelProvider.equals(ModelProvider.Google) ||
            modelProvider.equals(ModelProvider.Anthropic) ||
            modelProvider.equals(ModelProvider.OpenAI) ||
            modelProvider.equals(ModelProvider.Mistral) ||
            modelProvider.equals(ModelProvider.DeepSeek) ||
            modelProvider.equals(ModelProvider.OpenRouter) ||
            modelProvider.equals(ModelProvider.DeepInfra) ||
            modelProvider.equals(ModelProvider.Ollama);
    }

    private void addProjectToContext() {
        ModelProvider modelProvider = (ModelProvider) llmProvidersComboBox.getSelectedItem();
        if (modelProvider == null) {
            NotificationUtil.sendNotification(project, "Please select a provider first");
            return;
        }

        if (!isSupportedProvider(modelProvider)) {
            NotificationUtil.sendNotification(project,
                "This feature only works for OpenAI, Anthropic, Gemini and Ollama providers because of the large token window context.");
            return;
        }

        addProjectBtn.setEnabled(false);
        tokenUsageBar.setVisible(true);
        tokenUsageBar.reset();

        int tokenLimit = getWindowContext();

        ProjectContentService.getInstance().getProjectContent(project, tokenLimit, false)
            .thenAccept(projectContent -> {
                projectContext = "Project Context:\n" + projectContent.getContent();
                isProjectContextAdded = true;
                ApplicationManager.getApplication().invokeLater(() -> {
                    addProjectBtn.setIcon(DeleteIcon);
                    tokenCount = Encodings.newDefaultEncodingRegistry().getEncoding(EncodingType.CL100K_BASE).countTokens(projectContent.getContent());
                    addProjectBtn.setText(WindowContextFormatterUtil.format(tokenCount, "tokens"));
                    addProjectBtn.setToolTipText(REMOVE_ENTIRE_PROJECT_FROM_PROMPT_CONTEXT);
                    addProjectBtn.setEnabled(true);

                    tokenUsageBar.setTokens(tokenCount, tokenLimit);
                });
            })
            .exceptionally(ex -> {
                ApplicationManager.getApplication().invokeLater(() -> {
                    addProjectBtn.setEnabled(true);
                    tokenUsageBar.setVisible(false);
                    NotificationUtil.sendNotification(project, "Error adding project content: " + ex.getMessage());
                });
                return null;
            });
    }

    
    private int getWindowContext() {
        LanguageModel languageModel = (LanguageModel) modelNameComboBox.getSelectedItem();
        int tokenLimit = 4096;
        if (languageModel != null) {
            tokenLimit = languageModel.getContextWindow();
        }
        return tokenLimit;
    }

    private void updateButtonVisibility() {
        boolean isSupported = isProjectContextSupportedProvider();
        calcTokenCostBtn.setVisible(isSupported);
        addProjectBtn.setVisible(isSupported);
    }

    @Override
    public void settingsChanged(boolean hasKey) {
        calcProjectPanel.setVisible(hasKey && isProjectContextSupportedProvider());
        updateButtonVisibility();
    }

    private void calculateTokensAndCost() {
        LanguageModel selectedModel = (LanguageModel) modelNameComboBox.getSelectedItem();
        if (selectedModel == null) {
            NotificationUtil.sendNotification(project, "Please select a model first");
            return;
        }

        ModelProvider selectedProvider = (ModelProvider) llmProvidersComboBox.getSelectedItem();
        if (selectedProvider == null) {
            NotificationUtil.sendNotification(project, "Please select a provider first");
            return;
        }

        int maxTokens = selectedModel.getContextWindow();

        tokenCalculationService.calculateTokensAndCost(
            project,
            null,
            maxTokens,
            selectedProvider,
            selectedModel,
            DefaultLLMSettingsUtil.isApiKeyBasedProvider(selectedProvider));
    }

    public void updateTokenUsage(int maxTokens) {
        ApplicationManager.getApplication().invokeLater(() -> tokenUsageBar.setMaxTokens(maxTokens));
    }

    public void resetTokenUsageBar() {
        ApplicationManager.getApplication().invokeLater(() -> {
            tokenUsageBar.reset();
            tokenCount = 0;
        });
    }

    @Override
    public void onPromptSubmitted(@NotNull Project projectPrompt, String prompt) {
        if (!this.project.getName().equals(projectPrompt.getName())) {
            return;
        }
        ApplicationManager.getApplication().invokeLater(() -> {
            promptInputArea.setText(prompt);
            onSubmitPrompt(new ActionEvent(this, ActionEvent.ACTION_PERFORMED, Constant.SUBMIT_ACTION));
        });
    }

    private void setupAccessibility() {
        submitBtn.getAccessibleContext().setAccessibleDescription("Submit prompt to AI");
        addFileBtn.getAccessibleContext().setAccessibleDescription("Add files to context");

        // Add keyboard mnemonics
        submitBtn.setMnemonic('S');
        addFileBtn.setMnemonic('A');
    }

    private void confirmProjectContextRemoval() {
        int result = Messages.showYesNoDialog(project,
                "Are you sure you want to remove the project context?",
                "Confirm Removal",
                Messages.getQuestionIcon()
        );     if (result == Messages.YES) {
            removeProjectContext();
        }
    }
}

Key Improvements

  • Separation of Concerns: The ActionButtonsPanel is now primarily responsible for UI, while the ActionPanelController handles the logic.
  • Testability: The controller can be more easily tested in isolation.
  • Maintainability: The code is now more modular and easier to understand.

Next Steps

  1. Apply Refactorings: Apply the suggested refactorings to the rest of the codebase.
  2. Add Tests: Write unit tests for the controller classes and other logic-heavy components.
  3. Review and Iterate: Review the refactored code and make further improvements as needed.

By following these steps, you can significantly improve the readability, maintainability, and testability of your plugin. Remember to commit your changes frequently and test thoroughly after each refactoring step.


Gemini Exp 1206 Suggestions

Okay, I've analyzed the provided code structure and files for the DevoxxGenieIDEAPlugin project. Here are some crucial code refactorings I would suggest to improve readability, maintainability, and potentially performance:

1. Panel and Component Organization:

  • Extract smaller, reusable components: Several panels (e.g., SubmitPanel, ActionButtonsPanel, ChatResponsePanel) are quite large and contain a lot of UI setup logic. Consider breaking them down into smaller, more focused components. This will make them easier to understand, test, and modify independently.
  • Use layout managers effectively: Ensure consistent use of layout managers (e.g., BorderLayout, FlowLayout, BoxLayout, GridBagLayout) to create a clean and predictable UI structure. For example, in SubmitPanel, the use of nested BorderLayout panels could be simplified.
  • Consistent naming: Use consistent naming conventions for UI components (e.g., xxxPanel, xxxButton, xxxLabel).

2. Listener Management:

  • Centralize listener registration: Instead of adding listeners in various places, consider a more centralized approach. For example, you could have a dedicated class or method responsible for attaching listeners to UI components.
  • Use lambda expressions for brevity: Where appropriate, use lambda expressions to make listener code more concise.

3. Settings Management:

  • Consolidate settings access: The DevoxxGenieStateService is a good start for managing settings. However, consider creating a more structured way to access and modify settings, perhaps using dedicated methods for each setting or group of settings. This will improve type safety and reduce the risk of errors.
  • Use enums for settings: For settings with a limited set of options (e.g., GitDiffMode), use enums instead of booleans or strings. This will make the code more self-documenting and prevent invalid values.

4. Code Style and Formatting:

  • Consistent indentation and spacing: Ensure consistent indentation and spacing throughout the project. Use the IDE's auto-formatting capabilities to enforce this.
  • Meaningful variable and method names: Use descriptive names for variables and methods that clearly indicate their purpose.
  • Add comments where necessary: Add comments to explain complex logic or non-obvious code.

5. Error Handling:

  • Centralized error handling: The ErrorHandler class is a good start. Consider expanding it to handle different types of errors in a more granular way.
  • Use specific exception types: Instead of throwing generic IOException or RuntimeException, create custom exception types (e.g., ModelNotActiveException, ProviderUnavailableException) to provide more context about the error.

6. Asynchronous Operations:

  • Use CompletableFuture effectively: The project uses CompletableFuture for asynchronous operations. Ensure that these are used correctly and that error handling is properly implemented.
  • Avoid blocking the UI thread: Ensure that long-running operations (e.g., network requests, file I/O) are performed on background threads to prevent the UI from freezing.

7. Model and Data Handling:

  • Improve model representation: The LanguageModel class could be enhanced to include more information about the model's capabilities and limitations.
  • Data validation: Add validation to ensure that data received from external sources (e.g., API responses) is in the expected format and range.

8. Code Duplication:

  • Identify and eliminate code duplication: Look for duplicated code blocks and refactor them into reusable methods or classes. For example, there might be opportunities to refactor common UI setup logic.

9. Dependency Injection:

  • Consider using a dependency injection framework: For larger projects, a dependency injection framework (e.g., Guice, Dagger) can help manage dependencies and improve testability.

10. Testing:

  • Write unit tests: Add unit tests to cover critical parts of the code, especially the service layer and utility classes. This will help prevent regressions and improve code quality.

Example Refactoring (Extracting a smaller component):

Let's say in ActionButtonsPanel you have the following code to create the submitBtn:

submitBtn = new JHoverButton(SubmitIcon, false);
submitBtn.setToolTipText(SUBMIT_THE_PROMPT + SHIFT_ENTER);
submitBtn.setActionCommand(Constant.SUBMIT_ACTION);
submitBtn.addActionListener(this::onSubmitPrompt);
submitBtn.setMinimumSize(minSize);
submitBtn.setMaximumSize(maxSize);

You could extract this into a separate method or even a separate class:

private JButton createSubmitButton() {
    JButton button = new JHoverButton(SubmitIcon, false);
    button.setToolTipText(SUBMIT_THE_PROMPT + SHIFT_ENTER);
    button.setActionCommand(Constant.SUBMIT_ACTION);
    button.addActionListener(this::onSubmitPrompt);
    button.setMinimumSize(minSize);
    button.setMaximumSize(maxSize);
    return button;
}

Then, in your ActionButtonsPanel constructor:

submitBtn = createSubmitButton();
buttonPanel.add(submitBtn);

By implementing these refactorings, you can significantly improve the readability, maintainability, and overall quality of the DevoxxGenieIDEAPlugin project. Remember to test thoroughly after each refactoring step to ensure that no regressions are introduced.

@stephanj stephanj added the documentation Improvements or additions to documentation label Dec 11, 2024
@stephanj
Copy link
Contributor Author

A first refactoring has been made where we now have dedicated Controllers for the ActionsButtonPanel.
Not sure if we need to go all the way with dedicated ActionListeners and ActionEvent ?!

@samkerr4coding
Copy link
Contributor

Hello @stephanj i guess you did use the plugin (+ add directory feature) to get those detailed feedbacks but what was your exact prompt?

@stephanj
Copy link
Contributor Author

stephanj commented Dec 12, 2024

First of all make sure you either use Google "Gemini Exp 1206" (which is amazing) or "Gemini Flash 2.0 Exp".
Bot are currently experimental and free to use with a window context of 1M and 2M tokens !!

I then add the full project and ask : "Give me the top 10 improvements / code refactoring this project needs to increase readability".
Or "Refactor the ActionButtonsPanel so that we introduce a clean MVC implementation"

Basically very short prompts but the full project code is included 🥇

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
documentation Improvements or additions to documentation
Projects
None yet
Development

No branches or pull requests

2 participants