diff --git a/src/main/java/com/devoxx/genie/action/CalcTokensForDirectoryAction.java b/src/main/java/com/devoxx/genie/action/CalcTokensForDirectoryAction.java index cd963a64..1d37335e 100644 --- a/src/main/java/com/devoxx/genie/action/CalcTokensForDirectoryAction.java +++ b/src/main/java/com/devoxx/genie/action/CalcTokensForDirectoryAction.java @@ -1,8 +1,10 @@ package com.devoxx.genie.action; +import com.devoxx.genie.controller.listener.TokenCalculationListener; import com.devoxx.genie.model.enumarations.ModelProvider; import com.devoxx.genie.service.TokenCalculationService; import com.devoxx.genie.ui.settings.DevoxxGenieStateService; +import com.devoxx.genie.ui.util.NotificationUtil; import com.intellij.openapi.actionSystem.ActionUpdateThread; import com.intellij.openapi.actionSystem.AnActionEvent; import com.intellij.openapi.actionSystem.CommonDataKeys; @@ -11,11 +13,12 @@ import com.intellij.openapi.vfs.VirtualFile; import org.jetbrains.annotations.NotNull; -public class CalcTokensForDirectoryAction extends DumbAwareAction { +public class CalcTokensForDirectoryAction extends DumbAwareAction implements TokenCalculationListener { + private Project project; @Override public void actionPerformed(@NotNull AnActionEvent e) { - Project project = e.getProject(); + this.project = e.getProject(); VirtualFile selectedDir = e.getData(CommonDataKeys.VIRTUAL_FILE); if (project == null || selectedDir == null || !selectedDir.isDirectory()) { @@ -28,7 +31,7 @@ public void actionPerformed(@NotNull AnActionEvent e) { int maxTokens = stateService.getDefaultWindowContext(); new TokenCalculationService() - .calculateTokensAndCost(project, selectedDir, maxTokens, selectedProvider, null, false); + .calculateTokensAndCost(project, selectedDir, maxTokens, selectedProvider, null, false, this); } @@ -47,4 +50,9 @@ public void update(@NotNull AnActionEvent e) { public boolean isDumbAware() { return true; } + + @Override + public void onTokenCalculationComplete(String message) { + NotificationUtil.sendNotification(project, message); + } } diff --git a/src/main/java/com/devoxx/genie/controller/ActionButtonsPanelController.java b/src/main/java/com/devoxx/genie/controller/ActionButtonsPanelController.java new file mode 100644 index 00000000..8da8b929 --- /dev/null +++ b/src/main/java/com/devoxx/genie/controller/ActionButtonsPanelController.java @@ -0,0 +1,168 @@ +package com.devoxx.genie.controller; + +import com.devoxx.genie.chatmodel.ChatModelProvider; +import com.devoxx.genie.controller.listener.PromptExecutionListener; +import com.devoxx.genie.model.LanguageModel; +import com.devoxx.genie.model.enumarations.ModelProvider; +import com.devoxx.genie.model.request.ChatMessageContext; +import com.devoxx.genie.service.DevoxxGenieSettingsService; +import com.devoxx.genie.ui.EditorFileButtonManager; +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.devoxx.genie.util.ChatMessageContextUtil; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.ui.ComboBox; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import static com.devoxx.genie.model.enumarations.ModelProvider.*; + +public class ActionButtonsPanelController implements PromptExecutionListener { + + private final Project project; + private final EditorFileButtonManager editorFileButtonManager; + private final PromptInputArea promptInputArea; + private final ComboBox modelProviderComboBox; + private final ComboBox modelNameComboBox; + private final ChatModelProvider chatModelProvider = new ChatModelProvider(); + private final ActionButtonsPanel actionButtonsPanel; + private final PromptExecutionController promptExecutionController; + private final ProjectContextController projectContextController; + private final TokenCalculationController tokenCalculationController; + + public ActionButtonsPanelController(Project project, + PromptInputArea promptInputArea, + PromptOutputPanel promptOutputPanel, + ComboBox modelProviderComboBox, + ComboBox modelNameComboBox, + ActionButtonsPanel actionButtonsPanel) { + + this.project = project; + this.promptInputArea = promptInputArea; + this.editorFileButtonManager = new EditorFileButtonManager(project, null); + this.modelProviderComboBox = modelProviderComboBox; + this.modelNameComboBox = modelNameComboBox; + this.actionButtonsPanel = actionButtonsPanel; + this.promptExecutionController = new PromptExecutionController(project, promptInputArea, promptOutputPanel, actionButtonsPanel); + this.projectContextController = new ProjectContextController(project, modelProviderComboBox, modelNameComboBox, actionButtonsPanel); + this.tokenCalculationController = new TokenCalculationController(project, modelProviderComboBox, modelNameComboBox, actionButtonsPanel); + } + + public boolean isPromptRunning() { + return promptExecutionController.isPromptRunning(); + } + + public boolean handlePromptSubmission(String actionCommand, + boolean isProjectContextAdded, + String projectContext) { + + String userPromptText = getUserPromptText(); + + if (userPromptText == null) { + return false; + } + + ChatMessageContext currentChatMessageContext = + ChatMessageContextUtil.createContext(project, + userPromptText, + getSelectedLanguageModel(), + chatModelProvider, + actionCommand, + editorFileButtonManager, + projectContext, + isProjectContextAdded); + + return promptExecutionController.handlePromptSubmission(currentChatMessageContext); + } + + /** + * Stop the prompt execution. + */ + @Override + public void stopPromptExecution() { + promptExecutionController.stopPromptExecution(); + } + + @Override + public void startPromptExecution() { + promptExecutionController.startPromptExecution(); + } + + @Override + public void endPromptExecution() { + promptExecutionController.endPromptExecution(); + } + + private LanguageModel getSelectedLanguageModel() { + DevoxxGenieStateService stateService = DevoxxGenieStateService.getInstance(); + LanguageModel selectedLanguageModel = (LanguageModel) modelNameComboBox.getSelectedItem(); + + // If selectedLanguageModel is null, create a default one + if (selectedLanguageModel == null) { + selectedLanguageModel = createDefaultLanguageModel(stateService); + } + return selectedLanguageModel; + } + + /** + * get the user prompt text. + */ + private @Nullable String getUserPromptText() { + String userPromptText = promptInputArea.getText(); + if (userPromptText.isEmpty()) { + NotificationUtil.sendNotification(project, "Please enter a prompt."); + return null; + } + return userPromptText; + } + + /** + * Create a default language model. + * + * @param stateService the state service + * @return the default language model + */ + private LanguageModel createDefaultLanguageModel(@NotNull DevoxxGenieSettingsService stateService) { + ModelProvider selectedProvider = (ModelProvider) modelProviderComboBox.getSelectedItem(); + if (selectedProvider != null && + (selectedProvider.equals(LMStudio) || + selectedProvider.equals(GPT4All) || + selectedProvider.equals(Jlama) || + selectedProvider.equals(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 : OpenAI) + .modelName(modelName) + .apiKeyUsed(false) + .inputCost(0) + .outputCost(0) + .contextWindow(128_000) + .build(); + } + } + + /** + * Initiates the calculation of tokens and cost based on the selected model and provider. + * It delegates the actual calculation to the TokenCalculationController. + */ + public void calculateTokensAndCost() { + tokenCalculationController.calculateTokensAndCost(); + } + + public void updateButtonVisibility() { + boolean isSupported = projectContextController.isProjectContextSupportedProvider(); + actionButtonsPanel.setCalcTokenCostButtonVisible(isSupported); + actionButtonsPanel.setAddProjectButtonVisible(isSupported); + } +} diff --git a/src/main/java/com/devoxx/genie/controller/ActionPanelController.java b/src/main/java/com/devoxx/genie/controller/ActionPanelController.java deleted file mode 100644 index bf0965d5..00000000 --- a/src/main/java/com/devoxx/genie/controller/ActionPanelController.java +++ /dev/null @@ -1,181 +0,0 @@ -package com.devoxx.genie.controller; - -import com.devoxx.genie.chatmodel.ChatModelProvider; -import com.devoxx.genie.model.LanguageModel; -import com.devoxx.genie.model.enumarations.ModelProvider; -import com.devoxx.genie.model.request.ChatMessageContext; -import com.devoxx.genie.service.ChatPromptExecutor; -import com.devoxx.genie.service.DevoxxGenieSettingsService; -import com.devoxx.genie.ui.EditorFileButtonManager; -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.devoxx.genie.util.ChatMessageContextUtil; -import com.intellij.openapi.application.ApplicationManager; -import com.intellij.openapi.project.Project; -import com.intellij.openapi.ui.ComboBox; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -import java.util.concurrent.atomic.AtomicBoolean; - -public class ActionPanelController { - private final Project project; - private final ChatPromptExecutor chatPromptExecutor; - private final EditorFileButtonManager editorFileButtonManager; - private final PromptInputArea promptInputArea; - private final PromptOutputPanel promptOutputPanel; - private final ComboBox modelProviderComboBox; - private final ComboBox modelNameComboBox; - private final ChatModelProvider chatModelProvider = new ChatModelProvider(); - private final ActionButtonsPanel actionButtonsPanel; - private boolean isPromptRunning = false; - - private ChatMessageContext currentChatMessageContext; - - public ActionPanelController(Project project, - PromptInputArea promptInputArea, - PromptOutputPanel promptOutputPanel, - ComboBox modelProviderComboBox, - ComboBox modelNameComboBox, - ActionButtonsPanel actionButtonsPanel) { - - this.project = project; - this.promptInputArea = promptInputArea; - this.promptOutputPanel = promptOutputPanel; - this.chatPromptExecutor = new ChatPromptExecutor(promptInputArea); - this.editorFileButtonManager = new EditorFileButtonManager(project, null); - this.modelProviderComboBox = modelProviderComboBox; - this.modelNameComboBox = modelNameComboBox; - 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; - - AtomicBoolean response = new AtomicBoolean(true); - chatPromptExecutor.updatePromptWithCommandIfPresent(currentChatMessageContext, promptOutputPanel) - .ifPresentOrElse( - this::executePromptWithContext, - () -> response.set(false) - ); - - return response.get(); - } - - 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(); - } - - /** - * Validate and prepare the prompt. - * - * @param actionCommand the action event command - * @return true if the prompt is valid - */ - 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, - chatModelProvider, - stateService, - actionCommand, - editorFileButtonManager, - projectContext, - isProjectContextAdded - ); - - return true; - } - - /** - * get the user prompt text. - */ - private @Nullable String getUserPromptText() { - String userPromptText = promptInputArea.getText(); - if (userPromptText.isEmpty()) { - NotificationUtil.sendNotification(project, "Please enter a prompt."); - return null; - } - return userPromptText; - } - - /** - * Create a default language model. - * - * @param stateService the state service - * @return the default language model - */ - 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(); - } - } -} diff --git a/src/main/java/com/devoxx/genie/controller/ProjectContextController.java b/src/main/java/com/devoxx/genie/controller/ProjectContextController.java new file mode 100644 index 00000000..0778c575 --- /dev/null +++ b/src/main/java/com/devoxx/genie/controller/ProjectContextController.java @@ -0,0 +1,116 @@ +package com.devoxx.genie.controller; + +import com.devoxx.genie.model.LanguageModel; +import com.devoxx.genie.model.enumarations.ModelProvider; +import com.devoxx.genie.service.ProjectContentService; +import com.devoxx.genie.ui.panel.ActionButtonsPanel; +import com.devoxx.genie.ui.util.NotificationUtil; +import com.intellij.openapi.application.ApplicationManager; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.ui.ComboBox; +import com.knuddels.jtokkit.Encodings; +import com.knuddels.jtokkit.api.EncodingType; +import lombok.Getter; +import org.jetbrains.annotations.NotNull; + +import static com.devoxx.genie.model.enumarations.ModelProvider.*; + +public class ProjectContextController { + + private final Project project; + private final ComboBox modelProviderComboBox; + private final ComboBox modelNameComboBox; + private final ActionButtonsPanel actionButtonsPanel; + private boolean isProjectContextAdded = false; + @Getter + private String projectContext; + private int tokenCount; + + public ProjectContextController(Project project, + ComboBox modelProviderComboBox, + ComboBox modelNameComboBox, + ActionButtonsPanel actionButtonsPanel) { + this.project = project; + this.modelProviderComboBox = modelProviderComboBox; + this.modelNameComboBox = modelNameComboBox; + this.actionButtonsPanel = actionButtonsPanel; + } + + public void resetProjectContext() { + isProjectContextAdded = false; + projectContext = null; + tokenCount = 0; + actionButtonsPanel.updateAddProjectButton(isProjectContextAdded); + actionButtonsPanel.resetTokenUsageBar(); + NotificationUtil.sendNotification(project, "Project context removed successfully"); + } + + public void addProjectContext() { + ModelProvider modelProvider = (ModelProvider) modelProviderComboBox.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; + } + + actionButtonsPanel.setAddProjectButtonEnabled(false); + actionButtonsPanel.setTokenUsageBarVisible(true); + actionButtonsPanel.resetTokenUsageBar(); + + int tokenLimit = getWindowContext(); + + ProjectContentService.getInstance().getProjectContent(project, tokenLimit, false) + .thenAccept(projectContent -> { + projectContext = "Project Context:\n" + projectContent.getContent(); + isProjectContextAdded = true; + ApplicationManager.getApplication().invokeLater(() -> { + tokenCount = Encodings.newDefaultEncodingRegistry().getEncoding(EncodingType.CL100K_BASE).countTokens(projectContent.getContent()); + actionButtonsPanel.updateAddProjectButton(isProjectContextAdded, tokenCount); + actionButtonsPanel.setAddProjectButtonEnabled(true); + actionButtonsPanel.updateTokenUsageBar(tokenCount, tokenLimit); + }); + }) + .exceptionally(ex -> { + ApplicationManager.getApplication().invokeLater(() -> { + actionButtonsPanel.setAddProjectButtonEnabled(true); + actionButtonsPanel.setTokenUsageBarVisible(false); + NotificationUtil.sendNotification(project, "Error adding project content: " + ex.getMessage()); + }); + return null; + }); + } + + public boolean isProjectContextAdded() { + return isProjectContextAdded; + } + + public boolean isProjectContextSupportedProvider() { + ModelProvider selectedProvider = (ModelProvider) modelProviderComboBox.getSelectedItem(); + return selectedProvider != null && isSupportedProvider(selectedProvider); + } + + private boolean isSupportedProvider(@NotNull ModelProvider modelProvider) { + return modelProvider.equals(Google) || + modelProvider.equals(Anthropic) || + modelProvider.equals(OpenAI) || + modelProvider.equals(Mistral) || + modelProvider.equals(DeepSeek) || + modelProvider.equals(OpenRouter) || + modelProvider.equals(DeepInfra) || + modelProvider.equals(Ollama); + } + + private int getWindowContext() { + LanguageModel languageModel = (LanguageModel) modelNameComboBox.getSelectedItem(); + int tokenLimit = 4096; + if (languageModel != null) { + tokenLimit = languageModel.getContextWindow(); + } + return tokenLimit; + } +} diff --git a/src/main/java/com/devoxx/genie/controller/PromptExecutionController.java b/src/main/java/com/devoxx/genie/controller/PromptExecutionController.java new file mode 100644 index 00000000..80ea7603 --- /dev/null +++ b/src/main/java/com/devoxx/genie/controller/PromptExecutionController.java @@ -0,0 +1,89 @@ +package com.devoxx.genie.controller; + +import com.devoxx.genie.controller.listener.PromptExecutionListener; +import com.devoxx.genie.model.request.ChatMessageContext; +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.intellij.openapi.application.ApplicationManager; +import com.intellij.openapi.project.Project; + +import java.util.concurrent.atomic.AtomicBoolean; + +public class PromptExecutionController implements PromptExecutionListener { + + private final Project project; + private final ChatPromptExecutor chatPromptExecutor; + private final PromptInputArea promptInputArea; + private final PromptOutputPanel promptOutputPanel; + private final ActionButtonsPanel actionButtonsPanel; + private boolean isPromptRunning = false; + private ChatMessageContext currentChatMessageContext; + + public PromptExecutionController(Project project, + PromptInputArea promptInputArea, + PromptOutputPanel promptOutputPanel, + ActionButtonsPanel actionButtonsPanel) { + this.project = project; + this.promptInputArea = promptInputArea; + this.promptOutputPanel = promptOutputPanel; + this.chatPromptExecutor = new ChatPromptExecutor(promptInputArea); + this.actionButtonsPanel = actionButtonsPanel; + } + + public boolean isPromptRunning() { + return isPromptRunning; + } + + public boolean handlePromptSubmission(ChatMessageContext currentChatMessageContext) { + this.currentChatMessageContext = currentChatMessageContext; + + if (isPromptRunning) { + stopPromptExecution(); + return true; + } + + startPromptExecution(); + + isPromptRunning = true; + + AtomicBoolean response = new AtomicBoolean(true); + chatPromptExecutor.updatePromptWithCommandIfPresent(currentChatMessageContext, promptOutputPanel) + .ifPresentOrElse( + this::executePromptWithContext, + () -> response.set(false) // TODO Throw exception instead of returning false + ); + + return response.get(); + } + + private void executePromptWithContext(String command) { + chatPromptExecutor.executePrompt(currentChatMessageContext, promptOutputPanel, () -> { + endPromptExecution(); + ApplicationManager.getApplication().invokeLater(() -> { + promptInputArea.clear(); + promptInputArea.requestInputFocus(); + }); + }); + } + + @Override + public void stopPromptExecution() { + chatPromptExecutor.stopPromptExecution(project); + endPromptExecution(); + } + + @Override + public void startPromptExecution() { + actionButtonsPanel.disableSubmitBtn(); + actionButtonsPanel.disableButtons(); + actionButtonsPanel.startGlowing(); + } + + @Override + public void endPromptExecution() { + isPromptRunning = false; + actionButtonsPanel.enableButtons(); + } +} diff --git a/src/main/java/com/devoxx/genie/controller/TokenCalculationController.java b/src/main/java/com/devoxx/genie/controller/TokenCalculationController.java new file mode 100644 index 00000000..4b8b7394 --- /dev/null +++ b/src/main/java/com/devoxx/genie/controller/TokenCalculationController.java @@ -0,0 +1,69 @@ +package com.devoxx.genie.controller; + +import com.devoxx.genie.controller.listener.TokenCalculationListener; +import com.devoxx.genie.model.LanguageModel; +import com.devoxx.genie.model.enumarations.ModelProvider; +import com.devoxx.genie.service.TokenCalculationService; +import com.devoxx.genie.ui.util.NotificationUtil; +import com.devoxx.genie.util.DefaultLLMSettingsUtil; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.ui.ComboBox; + +public class TokenCalculationController { + + private final Project project; + private final ComboBox modelProviderComboBox; + private final ComboBox modelNameComboBox; + private final TokenCalculationService tokenCalculationService; + private TokenCalculationListener listener; + + public TokenCalculationController(Project project, + ComboBox modelProviderComboBox, + ComboBox modelNameComboBox, + TokenCalculationListener listener){ + this.project = project; + this.modelProviderComboBox = modelProviderComboBox; + this.modelNameComboBox = modelNameComboBox; + this.tokenCalculationService = new TokenCalculationService(); + this.listener = listener; + } + + /** + * Calculates the number of tokens and the associated cost for the current context. + * Displays the results in the token usage bar. + */ + public void calculateTokensAndCost() { + LanguageModel selectedModel = (LanguageModel) modelNameComboBox.getSelectedItem(); + ModelProvider selectedProvider = (ModelProvider) modelProviderComboBox.getSelectedItem(); + + if (selectedModel == null || selectedProvider == null) { + notifyModelOrProviderNotSelected(); + return; + } + + int maxTokens = selectedModel.getContextWindow(); + boolean isApiKeyBased = DefaultLLMSettingsUtil.isApiKeyBasedProvider(selectedProvider); + + // Perform the token and cost calculation + tokenCalculationService.calculateTokensAndCost( + project, + null, // Assuming file content is not needed for this calculation + maxTokens, + selectedProvider, + selectedModel, + isApiKeyBased, + listener + ); + } + + /** + * Notifies the user if either the model or provider is not selected. + */ + private void notifyModelOrProviderNotSelected() { + if (modelNameComboBox.getSelectedItem() == null) { + NotificationUtil.sendNotification(project, "Please select a model first"); + } else { + NotificationUtil.sendNotification(project, "Please select a provider first"); + } + } +} diff --git a/src/main/java/com/devoxx/genie/controller/listener/PromptExecutionListener.java b/src/main/java/com/devoxx/genie/controller/listener/PromptExecutionListener.java new file mode 100644 index 00000000..344aa337 --- /dev/null +++ b/src/main/java/com/devoxx/genie/controller/listener/PromptExecutionListener.java @@ -0,0 +1,10 @@ +package com.devoxx.genie.controller.listener; + +public interface PromptExecutionListener { + + void startPromptExecution(); + + void stopPromptExecution(); + + void endPromptExecution(); +} diff --git a/src/main/java/com/devoxx/genie/controller/listener/TokenCalculationListener.java b/src/main/java/com/devoxx/genie/controller/listener/TokenCalculationListener.java new file mode 100644 index 00000000..42b5c921 --- /dev/null +++ b/src/main/java/com/devoxx/genie/controller/listener/TokenCalculationListener.java @@ -0,0 +1,5 @@ +package com.devoxx.genie.controller.listener; + +public interface TokenCalculationListener { + void onTokenCalculationComplete(String message); +} \ No newline at end of file diff --git a/src/main/java/com/devoxx/genie/service/NonStreamingPromptExecutor.java b/src/main/java/com/devoxx/genie/service/NonStreamingPromptExecutor.java index 5412de73..c9ff391a 100644 --- a/src/main/java/com/devoxx/genie/service/NonStreamingPromptExecutor.java +++ b/src/main/java/com/devoxx/genie/service/NonStreamingPromptExecutor.java @@ -93,7 +93,6 @@ private void prompt(ChatMessageContext chatMessageContext, * Perform semantic search. * @param chatMessageContext the chat message context * @param promptOutputPanel the prompt output panel - * @param enableButtons the enable buttons */ private static void semanticSearch(ChatMessageContext chatMessageContext, @NotNull PromptOutputPanel promptOutputPanel, diff --git a/src/main/java/com/devoxx/genie/service/TokenCalculationService.java b/src/main/java/com/devoxx/genie/service/TokenCalculationService.java index 20e1405b..ee68745b 100644 --- a/src/main/java/com/devoxx/genie/service/TokenCalculationService.java +++ b/src/main/java/com/devoxx/genie/service/TokenCalculationService.java @@ -1,5 +1,6 @@ package com.devoxx.genie.service; +import com.devoxx.genie.controller.listener.TokenCalculationListener; import com.devoxx.genie.model.LanguageModel; import com.devoxx.genie.model.ScanContentResult; import com.devoxx.genie.model.enumarations.ModelProvider; @@ -26,7 +27,8 @@ public void calculateTokensAndCost(@NotNull Project project, int maxTokens, @NotNull ModelProvider selectedProvider, LanguageModel selectedLanguageModel, - boolean showCost) { + boolean showCost, + TokenCalculationListener listener) { CompletableFuture contentFuture; if (directory != null) { @@ -36,16 +38,17 @@ public void calculateTokensAndCost(@NotNull Project project, } if (showCost) { - showCostAndScanInfo(project, selectedProvider, selectedLanguageModel, contentFuture); + showCostAndScanInfo(project, selectedProvider, selectedLanguageModel, contentFuture, listener); } else { - showOnlyScanInfo(project, directory, selectedProvider, contentFuture); + showOnlyScanInfo(project, directory, selectedProvider, contentFuture, listener); } } private static void showOnlyScanInfo(@NotNull Project project, VirtualFile directory, @NotNull ModelProvider selectedProvider, - @NotNull CompletableFuture contentFuture) { + @NotNull CompletableFuture contentFuture, + TokenCalculationListener listener) { contentFuture.thenAccept(result -> { String message = String.format( "%s contains %s tokens using the %s tokenizer. " + @@ -56,32 +59,34 @@ private static void showOnlyScanInfo(@NotNull Project project, result.getFileCount(), result.getSkippedFileCount(), result.getSkippedDirectoryCount()); - NotificationUtil.sendNotification(project, message); + listener.onTokenCalculationComplete(message); }); } private void showCostAndScanInfo(@NotNull Project project, @NotNull ModelProvider selectedProvider, @NotNull LanguageModel languageModel, - CompletableFuture contentFuture) { + CompletableFuture contentFuture, + TokenCalculationListener listener) { if (!DefaultLLMSettingsUtil.isApiKeyBasedProvider(selectedProvider)) { contentFuture.thenAccept(scanResult -> { String defaultMessage = getDefaultMessage(scanResult); - NotificationUtil.sendNotification(project, defaultMessage); + listener.onTokenCalculationComplete(defaultMessage); }); } else { - showInfoForCloudProvider(project, selectedProvider, languageModel, contentFuture); + showInfoForCloudProvider(project, selectedProvider, languageModel, contentFuture, listener); } } private void showInfoForCloudProvider(@NotNull Project project, @NotNull ModelProvider selectedProvider, @NotNull LanguageModel languageModel, - @NotNull CompletableFuture contentFuture) { + @NotNull CompletableFuture contentFuture, + TokenCalculationListener listener) { Optional inputCost = LLMModelRegistryService.getInstance().getModels() .stream() .filter(model -> model.getProvider().getName().equals(selectedProvider.getName()) && - model.getModelName().equals(languageModel.getModelName())) + model.getModelName().equals(languageModel.getModelName())) .findFirst() .map(LanguageModel::getInputCost); @@ -89,35 +94,47 @@ private void showInfoForCloudProvider(@NotNull Project project, double estimatedInputCost = calculateCost(scanResult.getTokenCount(), aDouble); String message; if (scanResult.getSkippedFileCount() > 0 || scanResult.getSkippedDirectoryCount() > 0) { - message = String.format("%s Estimated cost using %s %s is $%.5f", - getDefaultMessage(scanResult), - selectedProvider.getName(), - languageModel.getDisplayName(), - estimatedInputCost - ); + message = getEstimatedCostMessage(selectedProvider, languageModel, scanResult, estimatedInputCost); } else { - message = String.format( - "Project contains %s in %d file%s. Estimated cost using %s %s is $%.5f", - WindowContextFormatterUtil.format(scanResult.getTokenCount(), "tokens"), - scanResult.getFileCount(), - scanResult.getFileCount() > 1 ? "s" : "", - selectedProvider.getName(), - languageModel.getDisplayName(), - estimatedInputCost - ); + message = getTotalFilesAndEstimatedCostMessage(selectedProvider, languageModel, scanResult, estimatedInputCost); } if (scanResult.getTokenCount() > languageModel.getContextWindow()) { message += String.format(". Total project size exceeds model's max context of %s tokens.", WindowContextFormatterUtil.format(languageModel.getContextWindow())); } - NotificationUtil.sendNotification(project, message); + listener.onTokenCalculationComplete(message); }), () -> { String message = "No input cost found for the selected model."; NotificationUtil.sendNotification(project, message); }); } + private @NotNull String getEstimatedCostMessage(@NotNull ModelProvider selectedProvider, @NotNull LanguageModel languageModel, ScanContentResult scanResult, double estimatedInputCost) { + String message; + message = String.format("%s Estimated cost using %s %s is $%.5f", + getDefaultMessage(scanResult), + selectedProvider.getName(), + languageModel.getDisplayName(), + estimatedInputCost + ); + return message; + } + + private static @NotNull String getTotalFilesAndEstimatedCostMessage(@NotNull ModelProvider selectedProvider, @NotNull LanguageModel languageModel, @NotNull ScanContentResult scanResult, double estimatedInputCost) { + String message; + message = String.format( + "Project contains %s in %d file%s. Estimated cost using %s %s is $%.5f", + WindowContextFormatterUtil.format(scanResult.getTokenCount(), "tokens"), + scanResult.getFileCount(), + scanResult.getFileCount() > 1 ? "s" : "", + selectedProvider.getName(), + languageModel.getDisplayName(), + estimatedInputCost + ); + return message; + } + private @NotNull String getDefaultMessage(@NotNull ScanContentResult scanResult) { return String.format( "%s from %d files, skipped " + diff --git a/src/main/java/com/devoxx/genie/ui/DevoxxGenieToolWindowContent.java b/src/main/java/com/devoxx/genie/ui/DevoxxGenieToolWindowContent.java index 408f3164..b85ea0e4 100644 --- a/src/main/java/com/devoxx/genie/ui/DevoxxGenieToolWindowContent.java +++ b/src/main/java/com/devoxx/genie/ui/DevoxxGenieToolWindowContent.java @@ -5,6 +5,7 @@ import com.devoxx.genie.model.enumarations.ModelProvider; import com.devoxx.genie.service.ConversationStorageService; import com.devoxx.genie.ui.component.border.AnimatedGlowingBorder; +import com.devoxx.genie.ui.listener.GlowingListener; import com.devoxx.genie.ui.listener.SettingsChangeListener; import com.devoxx.genie.ui.panel.ConversationPanel; import com.devoxx.genie.ui.panel.LlmProviderPanel; @@ -31,7 +32,7 @@ /** * The Devoxx Genie Tool Window Content. */ -public class DevoxxGenieToolWindowContent implements SettingsChangeListener { +public class DevoxxGenieToolWindowContent implements SettingsChangeListener, GlowingListener { private static final float SPLITTER_PROPORTION = 0.75f; private static final float MIN_PROPORTION = 0.3f; @@ -109,10 +110,12 @@ private void setupListeners() { llmProviderPanel.getModelNameComboBox().addActionListener(this::processModelNameSelection); } + @Override public void startGlowing() { animatedBorder.startGlowing(); } + @Override public void stopGlowing() { animatedBorder.stopGlowing(); } diff --git a/src/main/java/com/devoxx/genie/ui/component/border/AnimatedGlowingBorder.java b/src/main/java/com/devoxx/genie/ui/component/border/AnimatedGlowingBorder.java index a3e7e38d..7260f337 100644 --- a/src/main/java/com/devoxx/genie/ui/component/border/AnimatedGlowingBorder.java +++ b/src/main/java/com/devoxx/genie/ui/component/border/AnimatedGlowingBorder.java @@ -1,5 +1,6 @@ package com.devoxx.genie.ui.component.border; +import com.devoxx.genie.ui.listener.GlowingListener; import com.intellij.ui.JBColor; import org.jetbrains.annotations.NotNull; @@ -9,7 +10,7 @@ import java.awt.event.ActionEvent; import java.awt.event.ActionListener; -public class AnimatedGlowingBorder implements Border { +public class AnimatedGlowingBorder implements Border, GlowingListener { private final Timer glowTimer; private final GlowingBorder glowingBorder; private boolean isGlowing = false; @@ -19,8 +20,8 @@ public class AnimatedGlowingBorder implements Border { public AnimatedGlowingBorder(@NotNull JComponent component) { this.component = component; this.defaultBorder = BorderFactory.createEmptyBorder(4, 4, 4, 4); - this.glowingBorder = new GlowingBorder(new JBColor(new Color(0, 120, 215), - new Color(0, 120, 213))); + this.glowingBorder = new GlowingBorder( + new JBColor(new Color(0, 120, 215), new Color(0, 120, 213))); this.glowTimer = createGlowTimer(); } @@ -45,6 +46,7 @@ public void actionPerformed(ActionEvent e) { }); } + @Override public void startGlowing() { if (!isGlowing) { isGlowing = true; @@ -53,6 +55,7 @@ public void startGlowing() { } } + @Override public void stopGlowing() { if (isGlowing) { isGlowing = false; diff --git a/src/main/java/com/devoxx/genie/ui/listener/GlowingListener.java b/src/main/java/com/devoxx/genie/ui/listener/GlowingListener.java new file mode 100644 index 00000000..a4682960 --- /dev/null +++ b/src/main/java/com/devoxx/genie/ui/listener/GlowingListener.java @@ -0,0 +1,6 @@ +package com.devoxx.genie.ui.listener; + +public interface GlowingListener { + void startGlowing(); + void stopGlowing(); +} diff --git a/src/main/java/com/devoxx/genie/ui/panel/ActionButtonsPanel.java b/src/main/java/com/devoxx/genie/ui/panel/ActionButtonsPanel.java index 3a05ecda..db60b24e 100644 --- a/src/main/java/com/devoxx/genie/ui/panel/ActionButtonsPanel.java +++ b/src/main/java/com/devoxx/genie/ui/panel/ActionButtonsPanel.java @@ -1,23 +1,23 @@ package com.devoxx.genie.ui.panel; -import com.devoxx.genie.controller.ActionPanelController; +import com.devoxx.genie.controller.ActionButtonsPanelController; +import com.devoxx.genie.controller.ProjectContextController; +import com.devoxx.genie.controller.listener.TokenCalculationListener; 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.GlowingListener; 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; @@ -27,8 +27,6 @@ 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.*; @@ -41,35 +39,33 @@ import static com.devoxx.genie.model.Constant.*; import static com.devoxx.genie.ui.util.DevoxxGenieIconsUtil.*; -public class ActionButtonsPanel extends JPanel implements SettingsChangeListener, PromptSubmissionListener { +public class ActionButtonsPanel extends JPanel + implements SettingsChangeListener, PromptSubmissionListener, GlowingListener, TokenCalculationListener { 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 JButton addFileBtn; + private JButton submitBtn; + private JButton addProjectBtn; + private JButton calcTokenCostBtn; - private final PromptInputArea promptInputArea; - private final ComboBox llmProvidersComboBox; - private final ComboBox modelNameComboBox; - private final TokenUsageBar tokenUsageBar = new TokenUsageBar(); - private int tokenCount; + private final SubmitPanel submitPanel; - private final transient DevoxxGenieToolWindowContent devoxxGenieToolWindowContent; + private final JPanel calcProjectPanel = createCalcProjectPanel(); - private boolean isProjectContextAdded = false; - private String projectContext; + // Set minimum size for buttons to prevent them from becoming too small + private final Dimension minSize = new Dimension(100, 30); + private final Dimension maxSize = new Dimension(200, 30); - private final transient TokenCalculationService tokenCalculationService; - private final transient ActionPanelController controller; + private final PromptInputArea promptInputArea; + private final TokenUsageBar tokenUsageBar = createTokenUsageBar(); - private final JPanel buttonPanel = new JPanel(new FlowLayout(FlowLayout.LEFT, 5, 0)); - private final SubmitPanel submitPanel; + private final transient DevoxxGenieToolWindowContent devoxxGenieToolWindowContent; + + private final transient ActionButtonsPanelController controller; + private final transient ProjectContextController projectContextController; public ActionButtonsPanel(Project project, SubmitPanel submitPanel, @@ -86,83 +82,107 @@ public ActionButtonsPanel(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( + this.projectContextController = new ProjectContextController( + project, llmProvidersComboBox, modelNameComboBox, this); + + this.controller = new ActionButtonsPanelController( project, promptInputArea, promptOutputPanel, llmProvidersComboBox, modelNameComboBox, this ); - this.llmProvidersComboBox.addActionListener(e -> updateButtonVisibility()); + llmProvidersComboBox.addActionListener(e -> controller.updateButtonVisibility()); + // Call setupUI which will create the buttons before creating the button panel setupUI(); setupAccessibility(); setupMessageBus(); } private void setupUI() { + createButtons(); - // 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); + add(createProgressPanel(), BorderLayout.NORTH); + add(createButtonPanel(), BorderLayout.CENTER); + } - // Configure buttons - setupButtons(); + private void createButtons() { + submitBtn = createSubmitButton(); + addFileBtn = createAddFileButton(); + addProjectBtn = createAddProjectButton(); + calcTokenCostBtn = createCalcTokenCostButton(); + } - // Add button panel to main content - mainContent.add(buttonPanel, BorderLayout.CENTER); - add(mainContent, BorderLayout.CENTER); + private @NotNull JPanel createButtonPanel() { + JPanel buttonPanel = new JPanel(new GridLayout(1, 4, 5, 0)); + buttonPanel.add(submitBtn); + buttonPanel.add(calcTokenCostBtn); + buttonPanel.add(addProjectBtn); + buttonPanel.add(addFileBtn); + return buttonPanel; } - private void setupButtons() { - // Configure Submit button - submitBtn.setToolTipText(SUBMIT_THE_PROMPT + SHIFT_ENTER); - submitBtn.setActionCommand(Constant.SUBMIT_ACTION); - submitBtn.addActionListener(this::onSubmitPrompt); + private @NotNull TokenUsageBar createTokenUsageBar() { + TokenUsageBar tokenUsageBar = new TokenUsageBar(); + tokenUsageBar.setVisible(false); + tokenUsageBar.setPreferredSize(new Dimension(Integer.MAX_VALUE, 3)); + return tokenUsageBar; + } - // Configure Add File button - addFileBtn.setToolTipText(ADD_FILE_S_TO_PROMPT_CONTEXT); - addFileBtn.addActionListener(this::selectFilesForPromptContext); + private @NotNull JPanel createCalcProjectPanel() { + return new JPanel(new GridLayout(1, 2)); + } - calcTokenCostBtn.setToolTipText(CALCULATE_TOKENS_COST); - calcTokenCostBtn.addActionListener(e -> calculateTokensAndCost()); + private @NotNull JPanel createProgressPanel() { + JPanel progressPanel = new JPanel(new BorderLayout()); + progressPanel.add(tokenUsageBar, BorderLayout.CENTER); + return progressPanel; + } - addProjectBtn.setToolTipText(ADD_ENTIRE_PROJECT_TO_PROMPT_CONTEXT); - addProjectBtn.addActionListener(e -> { - if (isProjectContextAdded) { - confirmProjectContextRemoval(); - } else { - addProjectToContext(); - } - }); + private @NotNull JButton createCalcTokenCostButton() { + JButton button = new JHoverButton(CALC_TOKENS_COST, CalculateIcon, true); + button.setToolTipText(CALCULATE_TOKENS_COST); + button.addActionListener(e -> controller.calculateTokensAndCost()); + button.setMinimumSize(minSize); + button.setMaximumSize(maxSize); + return button; + } - buttonPanel.setLayout(new GridLayout(1, 4, 5, 0)); + private @NotNull JButton createAddProjectButton() { + JButton button = new JHoverButton(ADD_PROJECT_TO_CONTEXT, AddFileIcon, true); + button.setToolTipText(ADD_ENTIRE_PROJECT_TO_PROMPT_CONTEXT); + button.addActionListener(this::handleProjectContext); + button.setMinimumSize(minSize); + button.setMaximumSize(maxSize); + return button; + } - // Add buttons with horizontal glue between them - buttonPanel.add(submitBtn); - buttonPanel.add(calcTokenCostBtn); - buttonPanel.add(addProjectBtn); - buttonPanel.add(addFileBtn); + private void handleProjectContext(ActionEvent e) { + if (projectContextController.isProjectContextAdded()) { + confirmProjectContextRemoval(); + } else { + projectContextController.addProjectContext(); + } + } - // 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); + private @NotNull JButton createAddFileButton() { + JButton button = new JHoverButton(AddFileIcon, false); + button.setToolTipText(ADD_FILE_S_TO_PROMPT_CONTEXT); + button.addActionListener(this::selectFilesForPromptContext); + button.setMinimumSize(minSize); + button.setMaximumSize(maxSize); + return button; + } - // 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 @NotNull 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; } /** @@ -174,19 +194,19 @@ private void selectFilesForPromptContext(ActionEvent e) { 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(); + .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); + popup, + devoxxGenieToolWindowContent.getContentPanel().getSize().width, + promptInputArea.getLocationOnScreen().y); } } @@ -199,20 +219,15 @@ private void onSubmitPrompt(ActionEvent actionEvent) { return; } - disableUIForPromptExecution(); + boolean response = controller.handlePromptSubmission(actionEvent.getActionCommand(), + projectContextController.isProjectContextAdded(), + projectContextController.getProjectContext()); - boolean response = controller.executePrompt(actionEvent.getActionCommand(), isProjectContextAdded, projectContext); if (!response) { - enableButtons(); + controller.endPromptExecution(); } } - private void disableUIForPromptExecution() { - disableSubmitBtn(); - disableButtons(); - submitPanel.startGlowing(); - } - public void enableButtons() { ApplicationManager.getApplication().invokeLater(() -> { submitBtn.setIcon(SubmitIcon); @@ -222,7 +237,11 @@ public void enableButtons() { }); } - private void disableSubmitBtn() { + public void disableButtons() { + promptInputArea.setEnabled(false); + } + + public void disableSubmitBtn() { ApplicationManager.getApplication().invokeLater(() -> { submitBtn.setIcon(StopIcon); submitBtn.setToolTipText(PROMPT_IS_RUNNING_PLEASE_BE_PATIENT); @@ -235,181 +254,53 @@ private void setupMessageBus() { 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() { + projectContextController.resetProjectContext(); } - public void resetProjectContext() { - updateAddProjectButton(); + public void updateAddProjectButton(boolean isProjectContextAdded) { + updateAddProjectButton(isProjectContextAdded, 0); } - private void updateAddProjectButton() { + public void updateAddProjectButton(boolean isProjectContextAdded, int tokenCount) { if (isProjectContextAdded) { - addProjectBtn.setIcon(DeleteIcon); - addProjectBtn.setText(REMOVE_CONTEXT); - addProjectBtn.setToolTipText(REMOVE_ENTIRE_PROJECT_FROM_PROMPT_CONTEXT); + setAddProjectButton(DeleteIcon, REMOVE_CONTEXT, REMOVE_ENTIRE_PROJECT_FROM_PROMPT_CONTEXT); + if (tokenCount > 0) { + addProjectBtn.setText(WindowContextFormatterUtil.format(tokenCount, "tokens")); + } } else { - addProjectBtn.setIcon(AddFileIcon); - addProjectBtn.setText(ADD_PROJECT_TO_CONTEXT); - addProjectBtn.setToolTipText(ADD_ENTIRE_PROJECT_TO_PROMPT_CONTEXT); + setAddProjectButton(AddFileIcon, ADD_PROJECT_TO_CONTEXT, ADD_ENTIRE_PROJECT_TO_PROMPT_CONTEXT); } } - /** - * Check if the selected provider supports project context. - * Included also Ollama because of the Llama 3.1 release with a window context of 128K. - * - * @return true if the provider supports project context - */ - private boolean isProjectContextSupportedProvider() { - ModelProvider selectedProvider = (ModelProvider) llmProvidersComboBox.getSelectedItem(); - return selectedProvider != null && isSupportedProvider(selectedProvider); + private void setAddProjectButton(Icon addFileIcon, String addProjectToContext, String addEntireProjectToPromptContext) { + addProjectBtn.setIcon(addFileIcon); + addProjectBtn.setText(addProjectToContext); + addProjectBtn.setToolTipText(addEntireProjectToPromptContext); } - 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; - }); + public void setAddProjectButtonEnabled(boolean enabled) { + addProjectBtn.setEnabled(enabled); } - /** - * Get the window context for the selected provider and model. - * - * @return the token limit - */ - private int getWindowContext() { - LanguageModel languageModel = (LanguageModel) modelNameComboBox.getSelectedItem(); - int tokenLimit = 4096; - if (languageModel != null) { - tokenLimit = languageModel.getContextWindow(); - } - return tokenLimit; + public void setTokenUsageBarVisible(boolean visible) { + tokenUsageBar.setVisible(visible); } - private void updateButtonVisibility() { - boolean isSupported = isProjectContextSupportedProvider(); - calcTokenCostBtn.setVisible(isSupported); - addProjectBtn.setVisible(isSupported); + public void resetTokenUsageBar() { + ApplicationManager.getApplication().invokeLater(tokenUsageBar::reset); } @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)); + calcProjectPanel.setVisible(hasKey && projectContextController.isProjectContextSupportedProvider()); + controller.updateButtonVisibility(); } 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())) { @@ -435,8 +326,37 @@ private void confirmProjectContextRemoval() { "Are you sure you want to remove the project context?", "Confirm Removal", Messages.getQuestionIcon() - ); if (result == Messages.YES) { - removeProjectContext(); + ); + if (result == Messages.YES) { + projectContextController.resetProjectContext(); } } -} + + public void setCalcTokenCostButtonVisible(boolean visible) { + calcTokenCostBtn.setVisible(visible); + } + + public void setAddProjectButtonVisible(boolean visible) { + addProjectBtn.setVisible(visible); + } + + @Override + public void startGlowing() { + submitPanel.startGlowing(); + } + + @Override + public void stopGlowing() { + submitPanel.stopGlowing(); + } + + public void updateTokenUsageBar(int tokenCount, int tokenLimit) { + ApplicationManager.getApplication().invokeLater(() -> tokenUsageBar.setTokens(tokenCount, tokenLimit)); + } + + @Override + public void onTokenCalculationComplete(String message) { + NotificationUtil.sendNotification(project, message); + + } +} \ No newline at end of file diff --git a/src/main/java/com/devoxx/genie/ui/panel/SubmitPanel.java b/src/main/java/com/devoxx/genie/ui/panel/SubmitPanel.java index 12012ee8..4aab17af 100644 --- a/src/main/java/com/devoxx/genie/ui/panel/SubmitPanel.java +++ b/src/main/java/com/devoxx/genie/ui/panel/SubmitPanel.java @@ -2,56 +2,82 @@ import com.devoxx.genie.ui.DevoxxGenieToolWindowContent; import com.devoxx.genie.ui.component.input.PromptInputArea; +import com.devoxx.genie.ui.listener.GlowingListener; import com.intellij.openapi.project.Project; import com.intellij.ui.components.JBPanel; import com.intellij.ui.components.JBScrollPane; import lombok.Getter; -import org.jetbrains.annotations.Contract; import org.jetbrains.annotations.NotNull; import javax.swing.*; import java.awt.*; import java.util.ResourceBundle; -public class SubmitPanel extends JBPanel { +public class SubmitPanel extends JBPanel implements GlowingListener { + + private static final int MIN_INPUT_HEIGHT = 200; - public static final int MIN_INPUT_HEIGHT = 200; private final Project project; private final DevoxxGenieToolWindowContent toolWindowContent; + @Getter private final PromptInputArea promptInputArea; + @Getter - private ActionButtonsPanel actionButtonsPanel; + private final ActionButtonsPanel actionButtonsPanel; /** * The Submit Panel constructor. * * @param toolWindowContent the tool window content */ - public SubmitPanel(DevoxxGenieToolWindowContent toolWindowContent) { + public SubmitPanel(@NotNull DevoxxGenieToolWindowContent toolWindowContent) { super(new BorderLayout()); + this.toolWindowContent = toolWindowContent; this.project = toolWindowContent.getProject(); ResourceBundle resourceBundle = toolWindowContent.getResourceBundle(); - PromptContextFileListPanel promptContextFileListPanel = new PromptContextFileListPanel(project); promptInputArea = new PromptInputArea(resourceBundle, project); + actionButtonsPanel = createActionButtonsPanel(); + add(createSubmitPanel(actionButtonsPanel), BorderLayout.CENTER); + } + + /** + * The submit panel with the prompt input area and action buttons. + * @return the submit panel + */ + private @NotNull JPanel createSubmitPanel(ActionButtonsPanel actionButtonsPanel) { JPanel submitPanel = new JPanel(new BorderLayout()); submitPanel.setMinimumSize(new Dimension(0, MIN_INPUT_HEIGHT)); submitPanel.setPreferredSize(new Dimension(Integer.MAX_VALUE, MIN_INPUT_HEIGHT)); - - submitPanel.add(promptContextFileListPanel, BorderLayout.NORTH); + submitPanel.add(new PromptContextFileListPanel(project), BorderLayout.NORTH); submitPanel.add(new JBScrollPane(promptInputArea), BorderLayout.CENTER); - submitPanel.add(createActionButtonsPanel(), BorderLayout.SOUTH); + submitPanel.add(actionButtonsPanel, BorderLayout.SOUTH); + return submitPanel; + } - add(submitPanel); + /** + * The bottom action buttons panel (Submit, Search buttons and Add Files) + * @return the action buttons panel + */ + private @NotNull ActionButtonsPanel createActionButtonsPanel() { + return new ActionButtonsPanel(project, + this, + promptInputArea, + toolWindowContent.getPromptOutputPanel(), + toolWindowContent.getLlmProviderPanel().getModelProviderComboBox(), + toolWindowContent.getLlmProviderPanel().getModelNameComboBox(), + toolWindowContent); } + @Override public void startGlowing() { this.toolWindowContent.startGlowing(); } + @Override public void stopGlowing() { this.toolWindowContent.stopGlowing(); } @@ -60,21 +86,4 @@ public void stopGlowing() { public Dimension getMinimumSize() { return new Dimension(0, 150); } - - /** - * The bottom action buttons panel (Submit, Search buttons and Add Files) - * - * @return the action buttons panel - */ - @Contract(" -> new") - private @NotNull JPanel createActionButtonsPanel() { - actionButtonsPanel = new ActionButtonsPanel(project, - this, - promptInputArea, - toolWindowContent.getPromptOutputPanel(), - toolWindowContent.getLlmProviderPanel().getModelProviderComboBox(), - toolWindowContent.getLlmProviderPanel().getModelNameComboBox(), - toolWindowContent); - return actionButtonsPanel; - } } diff --git a/src/main/java/com/devoxx/genie/util/ChatMessageContextUtil.java b/src/main/java/com/devoxx/genie/util/ChatMessageContextUtil.java index 10b6c228..aa621f47 100644 --- a/src/main/java/com/devoxx/genie/util/ChatMessageContextUtil.java +++ b/src/main/java/com/devoxx/genie/util/ChatMessageContextUtil.java @@ -11,6 +11,7 @@ import com.devoxx.genie.service.FileListManager; import com.devoxx.genie.service.MessageCreationService; import com.devoxx.genie.ui.EditorFileButtonManager; +import com.devoxx.genie.ui.settings.DevoxxGenieStateService; import com.devoxx.genie.ui.util.EditorUtil; import com.intellij.openapi.editor.Editor; import com.intellij.openapi.project.Project; @@ -35,11 +36,13 @@ private ChatMessageContextUtil() { String userPromptText, LanguageModel languageModel, ChatModelProvider chatModelProvider, - @NotNull DevoxxGenieSettingsService stateService, @NotNull String actionCommand, EditorFileButtonManager editorFileButtonManager, String projectContext, boolean isProjectContextAdded) { + + DevoxxGenieStateService stateService = DevoxxGenieStateService.getInstance(); + ChatMessageContext context = ChatMessageContext.builder() .project(project) .id(String.valueOf(System.currentTimeMillis())) diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index eb97817c..5e726a60 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -1 +1,2 @@ +#Thu Dec 12 13:52:02 CET 2024 version=0.4.3 diff --git a/src/test/java/com/devoxx/genie/service/PromptExecutionServiceIT.java b/src/test/java/com/devoxx/genie/service/PromptExecutionServiceIT.java index e4415bee..1fc0f7bf 100644 --- a/src/test/java/com/devoxx/genie/service/PromptExecutionServiceIT.java +++ b/src/test/java/com/devoxx/genie/service/PromptExecutionServiceIT.java @@ -56,7 +56,7 @@ private void mockSettingsState() { @Test public void testExecuteQueryOpenAI() { LanguageModel model = LanguageModel.builder() - .provider(ModelProvider.OPENAI) + .provider(ModelProvider.OpenAI) .modelName("gpt-3.5-turbo") .displayName("GPT-3.5 Turbo") .apiKeyUsed(true) @@ -70,7 +70,7 @@ public void testExecuteQueryOpenAI() { @Test public void testExecuteQueryAnthropic() { LanguageModel model = LanguageModel.builder() - .provider(ModelProvider.ANTHROPIC) + .provider(ModelProvider.Anthropic) .modelName("claude-3-5-sonnet-20240620") .displayName("claude-3-5-sonnet-20240620") .apiKeyUsed(true) @@ -84,7 +84,7 @@ public void testExecuteQueryAnthropic() { @Test public void testExecuteQueryGemini() { LanguageModel model = LanguageModel.builder() - .provider(ModelProvider.GOOGLE) + .provider(ModelProvider.Google) .modelName("gemini-1.5-flash") .displayName("Gemini 1.5 Flash") .apiKeyUsed(true) @@ -98,7 +98,7 @@ public void testExecuteQueryGemini() { @Test public void testExecuteQueryMistral() { LanguageModel model = LanguageModel.builder() - .provider(ModelProvider.MISTRAL) + .provider(ModelProvider.Mistral) .modelName("mistral-medium") .displayName("Mistral Medium") .apiKeyUsed(true) @@ -112,7 +112,7 @@ public void testExecuteQueryMistral() { @Test public void testExecuteQueryDeepInfra() { LanguageModel model = LanguageModel.builder() - .provider(ModelProvider.DEEP_INFRA) + .provider(ModelProvider.DeepInfra) .modelName("mistralai/Mixtral-8x7B-Instruct-v0.1") .displayName("Mixtral 8x7B") .apiKeyUsed(true) @@ -125,23 +125,23 @@ public void testExecuteQueryDeepInfra() { private ChatLanguageModel createChatModel(LanguageModel languageModel) { return switch (languageModel.getProvider()) { - case OPENAI -> OpenAiChatModel.builder() + case OpenAI -> OpenAiChatModel.builder() .apiKey(dotenv.get("OPENAI_API_KEY")) .modelName(languageModel.getModelName()) .build(); - case ANTHROPIC -> AnthropicChatModel.builder() + case Anthropic -> AnthropicChatModel.builder() .apiKey(dotenv.get("ANTHROPIC_API_KEY")) .modelName(languageModel.getModelName()) .build(); - case GOOGLE -> GoogleAiGeminiChatModel.builder() + case Google -> GoogleAiGeminiChatModel.builder() .apiKey(dotenv.get("GEMINI_API_KEY")) .modelName(languageModel.getModelName()) .build(); - case MISTRAL -> MistralAiChatModel.builder() + case Mistral -> MistralAiChatModel.builder() .apiKey(dotenv.get("MISTRAL_API_KEY")) .modelName(languageModel.getModelName()) .build(); - case DEEP_INFRA -> OpenAiChatModel.builder() + case DeepInfra -> OpenAiChatModel.builder() .baseUrl("https://api.deepinfra.com/v1/openai") .apiKey(dotenv.get("DEEPINFRA_API_KEY")) .modelName(languageModel.getModelName())