diff --git a/build.gradle.kts b/build.gradle.kts index 3f217291..28d03005 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -18,6 +18,9 @@ dependencies { implementation("dev.langchain4j:langchain4j-open-ai:0.31.0") implementation("dev.langchain4j:langchain4j-anthropic:0.31.0") implementation("dev.langchain4j:langchain4j-mistral-ai:0.31.0") + implementation("dev.langchain4j:langchain4j-web-search-engine-google-custom:0.31.0") + implementation("dev.langchain4j:langchain4j-web-search-engine-tavily:0.31.0") + implementation("org.commonmark:commonmark:0.22.0") compileOnly("org.projectlombok:lombok:1.18.32") diff --git a/src/main/java/com/devoxx/genie/chatmodel/ChatModelProvider.java b/src/main/java/com/devoxx/genie/chatmodel/ChatModelProvider.java index d9bdf982..e1794796 100644 --- a/src/main/java/com/devoxx/genie/chatmodel/ChatModelProvider.java +++ b/src/main/java/com/devoxx/genie/chatmodel/ChatModelProvider.java @@ -68,7 +68,7 @@ public StreamingChatLanguageModel getStreamingChatLanguageModel(@NotNull ChatMes * @return the chat model factory */ private @NotNull ChatModelFactory getFactory(@NotNull ChatMessageContext chatMessageContext) { - ModelProvider provider = ModelProvider.valueOf(chatMessageContext.getLlmProvider()); + ModelProvider provider = ModelProvider.fromString(chatMessageContext.getLlmProvider()); ChatModelFactory factory = factories.get(provider); if (factory == null) { throw new IllegalArgumentException("No factory for provider: " + provider); diff --git a/src/main/java/com/devoxx/genie/model/Constant.java b/src/main/java/com/devoxx/genie/model/Constant.java index 6257579c..2e7fde45 100644 --- a/src/main/java/com/devoxx/genie/model/Constant.java +++ b/src/main/java/com/devoxx/genie/model/Constant.java @@ -1,6 +1,7 @@ package com.devoxx.genie.model; public class Constant { + private Constant() { } @@ -10,12 +11,21 @@ private Constant() { public static final String EXPLAIN_PROMPT = "Break down the code in simple terms to help a junior developer grasp its functionality."; public static final String CUSTOM_PROMPT = "Write a custom prompt here."; - // The Local LLM Model URLs + // The Local LLM Model URLs, these can be overridden in the settings page public static final String OLLAMA_MODEL_URL = "http://localhost:11434/"; public static final String LMSTUDIO_MODEL_URL = "http://localhost:1234/v1/"; public static final String GPT4ALL_MODEL_URL = "http://localhost:4891/v1/"; public static final String JAN_MODEL_URL = "http://localhost:1337/v1/"; + // ActionCommands + public static final String SUBMIT_ACTION = "submit"; + public static final String TAVILY_SEARCH_ACTION = "tavilySearch"; + public static final String GOOGLE_SEARCH_ACTION = "googleSearch"; + public static final String COMBO_BOX_CHANGED = "comboBoxChanged"; + + // I18N file name + public static final String MESSAGES = "messages"; + // The LLM Settings public static final Double TEMPERATURE = 0.7d; public static final Double TOP_P = 0.9d; @@ -24,10 +34,23 @@ private Constant() { public static final Integer TIMEOUT = 60; public static final Integer MAX_MEMORY = 10; + // Hide Search Button + public static final Boolean HIDE_SEARCH_BUTTONS = false; + + // Stream mode settings public static final Boolean STREAM_MODE = false; + // AST settings public static final Boolean AST_MODE = false; public static final Boolean AST_PARENT_CLASS = true; public static final Boolean AST_CLASS_REFERENCE = true; public static final Boolean AST_FIELD_REFERENCE = true; + + // Button tooltip texts + public static final String ADD_FILE_S_TO_PROMPT_CONTEXT = "Add file(s) to prompt context"; + public static final String SUBMIT_THE_PROMPT = "Submit the prompt"; + public static final String SEARCH_THE_WEB_WITH_TAVILY_FOR_AN_ANSWER = "Search the web with Tavily for an answer"; + public static final String SEARCH_GOOGLE_FOR_AN_ANSWER = "Search Google for an answer"; + public static final String PROMPT_IS_RUNNING_PLEASE_BE_PATIENT = "Prompt is running, please be patient..."; + } diff --git a/src/main/java/com/devoxx/genie/model/enumarations/ModelProvider.java b/src/main/java/com/devoxx/genie/model/enumarations/ModelProvider.java index a6688d3c..2a8507b9 100644 --- a/src/main/java/com/devoxx/genie/model/enumarations/ModelProvider.java +++ b/src/main/java/com/devoxx/genie/model/enumarations/ModelProvider.java @@ -1,6 +1,7 @@ package com.devoxx.genie.model.enumarations; import lombok.Getter; +import org.jetbrains.annotations.NotNull; @Getter public enum ModelProvider { @@ -21,4 +22,12 @@ public enum ModelProvider { this.name = name; } + public static @NotNull ModelProvider fromString(String name) { + for (ModelProvider provider : ModelProvider.values()) { + if (provider.getName().equals(name)) { + return provider; + } + } + throw new IllegalArgumentException("No enum found with name: [" + name + "]"); + } } diff --git a/src/main/java/com/devoxx/genie/service/ChatPromptExecutor.java b/src/main/java/com/devoxx/genie/service/ChatPromptExecutor.java index 4e993977..e76a3ad4 100644 --- a/src/main/java/com/devoxx/genie/service/ChatPromptExecutor.java +++ b/src/main/java/com/devoxx/genie/service/ChatPromptExecutor.java @@ -1,5 +1,6 @@ package com.devoxx.genie.service; +import com.devoxx.genie.model.Constant; import com.devoxx.genie.model.request.ChatMessageContext; import com.devoxx.genie.ui.panel.PromptOutputPanel; import com.devoxx.genie.ui.util.NotificationUtil; @@ -9,6 +10,7 @@ import dev.langchain4j.model.chat.StreamingChatLanguageModel; import org.jetbrains.annotations.NotNull; +import java.awt.event.ActionEvent; import java.util.Optional; import java.util.concurrent.CancellationException; import java.util.concurrent.TimeoutException; @@ -23,7 +25,6 @@ public ChatPromptExecutor() { /** * Execute the prompt. - * * @param chatMessageContext the chat message context * @param promptOutputPanel the prompt output panel * @param enableButtons the Enable buttons @@ -35,19 +36,39 @@ public void executePrompt(@NotNull ChatMessageContext chatMessageContext, new Task.Backgroundable(chatMessageContext.getProject(), "Working...", true) { @Override public void run(@NotNull ProgressIndicator progressIndicator) { - if (SettingsStateService.getInstance().getStreamMode()) { - setupStreaming(chatMessageContext, promptOutputPanel, enableButtons); + if (chatMessageContext.getContext().toLowerCase().contains("search")) { + webSearchPrompt(chatMessageContext, promptOutputPanel, enableButtons); } else { - runPrompt(chatMessageContext, promptOutputPanel, enableButtons); - progressIndicator.setText("Working..."); + if (SettingsStateService.getInstance().getStreamMode()) { + setupStreaming(chatMessageContext, promptOutputPanel, enableButtons); + } else { + runPrompt(chatMessageContext, promptOutputPanel, enableButtons); + } } } }.queue(); } + /** + * Web search prompt. + * @param chatMessageContext the chat message context + * @param promptOutputPanel the prompt output panel + * @param enableButtons the Enable buttons + */ + private void webSearchPrompt(@NotNull ChatMessageContext chatMessageContext, + @NotNull PromptOutputPanel promptOutputPanel, + Runnable enableButtons) { + promptOutputPanel.addUserPrompt(chatMessageContext); + WebSearchService.getInstance().searchWeb(chatMessageContext) + .ifPresent(aiMessage -> { + chatMessageContext.setAiMessage(aiMessage); + promptOutputPanel.addChatResponse(chatMessageContext); + enableButtons.run(); + }); + } + /** * Process possible command prompt. - * * @param chatMessageContext the chat message context * @param promptOutputPanel the prompt output panel */ @@ -59,7 +80,6 @@ public void updatePromptWithCommandIfPresent(@NotNull ChatMessageContext chatMes /** * Setup streaming. - * * @param chatMessageContext the chat message context * @param promptOutputPanel the prompt output panel * @param enableButtons the Enable buttons @@ -121,7 +141,6 @@ private Optional getCommandFromPrompt(@NotNull String prompt, /** * Run the prompt. - * * @param chatMessageContext the chat message context * @param promptOutputPanel the prompt output panel * @param enableButtons the Enable buttons @@ -130,7 +149,6 @@ private void runPrompt(@NotNull ChatMessageContext chatMessageContext, PromptOutputPanel promptOutputPanel, Runnable enableButtons) { - promptExecutionService.executeQuery(chatMessageContext) .thenAccept(aiMessageOptional -> { enableButtons.run(); diff --git a/src/main/java/com/devoxx/genie/service/MessageCreationService.java b/src/main/java/com/devoxx/genie/service/MessageCreationService.java index 0617ff8a..f8d0422b 100644 --- a/src/main/java/com/devoxx/genie/service/MessageCreationService.java +++ b/src/main/java/com/devoxx/genie/service/MessageCreationService.java @@ -2,7 +2,11 @@ import com.devoxx.genie.model.request.ChatMessageContext; import com.devoxx.genie.model.request.EditorInfo; +import com.devoxx.genie.ui.util.NotificationUtil; import com.intellij.openapi.application.ApplicationManager; +import com.intellij.openapi.editor.Document; +import com.intellij.openapi.fileEditor.FileDocumentManager; +import com.intellij.openapi.project.Project; import com.intellij.openapi.vfs.VirtualFile; import dev.langchain4j.data.message.SystemMessage; import dev.langchain4j.data.message.UserMessage; @@ -12,6 +16,8 @@ import java.util.List; import java.util.Optional; +import static com.devoxx.genie.action.AddSnippetAction.SELECTED_TEXT_KEY; + /** * The message creation service for user and system messages. * Here's where also the basic prompt "engineering" is happening, including calling the AST magic. @@ -73,13 +79,45 @@ public static MessageCreationService getInstance() { return userMessage; } + /** + * Create user prompt with context. + * @param project the project + * @param userPrompt the user prompt + * @param files the files + * @return the user prompt with context + */ + public @NotNull String createUserPromptWithContext(Project project, + String userPrompt, + @NotNull List files) { + StringBuilder userPromptContext = new StringBuilder(); + FileDocumentManager fileDocumentManager = FileDocumentManager.getInstance(); + files.forEach(file -> ApplicationManager.getApplication().runReadAction(() -> { + if (file.getFileType().getName().equals("UNKNOWN")) { + userPromptContext.append("Filename: ").append(file.getName()).append("\n"); + userPromptContext.append("Code Snippet: ").append(file.getUserData(SELECTED_TEXT_KEY)).append("\n"); + } else { + Document document = fileDocumentManager.getDocument(file); + if (document != null) { + userPromptContext.append("Filename: ").append(file.getName()).append("\n"); + String content = document.getText(); + userPromptContext.append(content).append("\n"); + } else { + NotificationUtil.sendNotification(project, "Error reading file: " + file.getName()); + } + } + })); + + userPromptContext.append(userPrompt); + return userPromptContext.toString(); + } + /** * Construct a user message with context. * @param chatMessageContext the chat message context * @param context the context * @return the user message */ - private static @NotNull UserMessage constructUserMessage(@NotNull ChatMessageContext chatMessageContext, + private @NotNull UserMessage constructUserMessage(@NotNull ChatMessageContext chatMessageContext, String context) { StringBuilder sb = new StringBuilder(QUESTION); @@ -108,7 +146,7 @@ public static MessageCreationService getInstance() { * @param chatMessageContext the chat message context * @param sb the string builder */ - private static void addASTContext(@NotNull ChatMessageContext chatMessageContext, + private void addASTContext(@NotNull ChatMessageContext chatMessageContext, @NotNull StringBuilder sb) { sb.append("\n\nRelated classes:\n\n"); List tempFiles = new ArrayList<>(); @@ -129,7 +167,7 @@ private static void addASTContext(@NotNull ChatMessageContext chatMessageContext * @param sb the string builder * @param text the text */ - private static void appendIfNotEmpty(StringBuilder sb, String text) { + private void appendIfNotEmpty(StringBuilder sb, String text) { if (text != null && !text.isEmpty()) { sb.append(text).append("\n"); } diff --git a/src/main/java/com/devoxx/genie/service/PromptExecutionService.java b/src/main/java/com/devoxx/genie/service/PromptExecutionService.java index ebca4ed0..e6e1c71e 100644 --- a/src/main/java/com/devoxx/genie/service/PromptExecutionService.java +++ b/src/main/java/com/devoxx/genie/service/PromptExecutionService.java @@ -7,6 +7,7 @@ import dev.langchain4j.data.message.UserMessage; import dev.langchain4j.model.chat.ChatLanguageModel; import dev.langchain4j.model.output.Response; + import lombok.Getter; import org.jetbrains.annotations.NotNull; @@ -54,8 +55,9 @@ static PromptExecutionService getInstance() { ChatMemoryService.getInstance().add(userMessage); - queryFuture = CompletableFuture.supplyAsync(() -> processChatMessage(chatMessageContext), queryExecutor) - .orTimeout(chatMessageContext.getTimeout(), TimeUnit.SECONDS); + queryFuture = CompletableFuture.supplyAsync(() -> + processChatMessage(chatMessageContext), queryExecutor) + .orTimeout(chatMessageContext.getTimeout(), TimeUnit.SECONDS); } finally { queryLock.unlock(); } @@ -79,7 +81,6 @@ private boolean isCanceled() { /** * Process the chat message. - * * @param chatMessageContext the chat message context * @return the AI message */ diff --git a/src/main/java/com/devoxx/genie/service/SettingsStateService.java b/src/main/java/com/devoxx/genie/service/SettingsStateService.java index 90af85d9..7653c8a3 100644 --- a/src/main/java/com/devoxx/genie/service/SettingsStateService.java +++ b/src/main/java/com/devoxx/genie/service/SettingsStateService.java @@ -1,6 +1,5 @@ package com.devoxx.genie.service; -import com.devoxx.genie.model.Constant; import com.devoxx.genie.ui.util.DoubleConverter; import com.intellij.openapi.application.ApplicationManager; import com.intellij.openapi.components.PersistentStateComponent; @@ -13,6 +12,8 @@ import lombok.Setter; import org.jetbrains.annotations.NotNull; +import static com.devoxx.genie.model.Constant.*; + @Getter @Setter @Service @@ -27,10 +28,10 @@ public static SettingsStateService getInstance() { } // Local LLM URL fields - private String ollamaModelUrl = Constant.OLLAMA_MODEL_URL; - private String lmstudioModelUrl = Constant.LMSTUDIO_MODEL_URL; - private String gpt4allModelUrl = Constant.GPT4ALL_MODEL_URL; - private String janModelUrl = Constant.JAN_MODEL_URL; + private String ollamaModelUrl = OLLAMA_MODEL_URL; + private String lmstudioModelUrl = LMSTUDIO_MODEL_URL; + private String gpt4allModelUrl = GPT4ALL_MODEL_URL; + private String janModelUrl = JAN_MODEL_URL; // LLM API Keys private String openAIKey = ""; @@ -41,38 +42,44 @@ public static SettingsStateService getInstance() { private String deepInfraKey = ""; private String geminiKey = ""; + // Search API Keys + private Boolean hideSearchButtonsFlag = HIDE_SEARCH_BUTTONS; + private String googleSearchKey = ""; + private String googleCSIKey = ""; + private String tavilySearchKey = ""; + // Prompt fields - private String testPrompt = Constant.TEST_PROMPT; - private String reviewPrompt = Constant.REVIEW_PROMPT; - private String explainPrompt = Constant.EXPLAIN_PROMPT; - private String customPrompt = Constant.CUSTOM_PROMPT; + private String testPrompt = TEST_PROMPT; + private String reviewPrompt = REVIEW_PROMPT; + private String explainPrompt = EXPLAIN_PROMPT; + private String customPrompt = CUSTOM_PROMPT; // LLM settings @OptionTag(converter = DoubleConverter.class) - private Double temperature = Constant.TEMPERATURE; + private Double temperature = TEMPERATURE; @OptionTag(converter = DoubleConverter.class) - private Double topP = Constant.TOP_P; + private Double topP = TOP_P; - private Integer timeout = Constant.TIMEOUT; - private Integer maxRetries = Constant.MAX_RETRIES; - private Integer chatMemorySize = Constant.MAX_MEMORY; + private Integer timeout = TIMEOUT; + private Integer maxRetries = MAX_RETRIES; + private Integer chatMemorySize = MAX_MEMORY; // Was unable to make it work with Integer for some unknown reason - private String maxOutputTokens = Constant.MAX_OUTPUT_TOKENS.toString(); + private String maxOutputTokens = MAX_OUTPUT_TOKENS.toString(); // Last selected LLM provider and model name private String lastSelectedProvider; private String lastSelectedModel; // Enable stream mode - private Boolean streamMode = Constant.STREAM_MODE; + private Boolean streamMode = STREAM_MODE; // Enable AST mode - private Boolean astMode = Constant.AST_MODE; - private Boolean astParentClass = Constant.AST_PARENT_CLASS; - private Boolean astClassReference = Constant.AST_CLASS_REFERENCE; - private Boolean astFieldReference = Constant.AST_FIELD_REFERENCE; + private Boolean astMode = AST_MODE; + private Boolean astParentClass = AST_PARENT_CLASS; + private Boolean astClassReference = AST_CLASS_REFERENCE; + private Boolean astFieldReference = AST_FIELD_REFERENCE; @Override public SettingsStateService getState() { diff --git a/src/main/java/com/devoxx/genie/service/WebSearchService.java b/src/main/java/com/devoxx/genie/service/WebSearchService.java new file mode 100644 index 00000000..29865821 --- /dev/null +++ b/src/main/java/com/devoxx/genie/service/WebSearchService.java @@ -0,0 +1,87 @@ +package com.devoxx.genie.service; + +import com.devoxx.genie.model.request.ChatMessageContext; + +import com.intellij.openapi.application.ApplicationManager; +import dev.langchain4j.data.message.AiMessage; +import dev.langchain4j.rag.content.retriever.ContentRetriever; +import dev.langchain4j.rag.content.retriever.WebSearchContentRetriever; +import dev.langchain4j.service.AiServices; +import dev.langchain4j.service.SystemMessage; +import dev.langchain4j.web.search.WebSearchEngine; +import dev.langchain4j.web.search.google.customsearch.GoogleCustomWebSearchEngine; +import dev.langchain4j.web.search.tavily.TavilyWebSearchEngine; +import org.jetbrains.annotations.NotNull; + +import java.util.Optional; + +import static com.devoxx.genie.model.Constant.GOOGLE_SEARCH_ACTION; +import static com.devoxx.genie.model.Constant.TAVILY_SEARCH_ACTION; + +public class WebSearchService { + + public static WebSearchService getInstance() { + return ApplicationManager.getApplication().getService(WebSearchService.class); + } + + interface SearchWebsite { + @SystemMessage(""" + Provide a paragraph-long answer, not a long step by step explanation. + Reply with "I don't know the answer" if the provided information isn't relevant. + """) + String search(String query); + } + + /** + * Search the web for the given query. + * @param chatMessageContext the chat message context + * @return the AI message + */ + public @NotNull Optional searchWeb(@NotNull ChatMessageContext chatMessageContext) { + return getWebSearchEngine(chatMessageContext) + .flatMap(webSearchEngine -> executeSearchCommand(webSearchEngine, chatMessageContext)); + } + + /** + * Execute the search command. + * @param webSearchEngine the web search engine + * @param chatMessageContext the chat message context + * @return the AI message + */ + private @NotNull Optional executeSearchCommand(WebSearchEngine webSearchEngine, + @NotNull ChatMessageContext chatMessageContext) { + ContentRetriever contentRetriever = WebSearchContentRetriever.builder() + .webSearchEngine(webSearchEngine) + .maxResults(3) // TODO Move value to Settings page + .build(); + + SearchWebsite website = AiServices.builder(SearchWebsite.class) + .chatLanguageModel(chatMessageContext.getChatLanguageModel()) + .contentRetriever(contentRetriever) + .build(); + + return Optional.of(new AiMessage(website.search(chatMessageContext.getUserPrompt()))); + } + + /** + * Get the web search engine. + * @param chatMessageContext the chat message context + * @return the web search engine + */ + private static Optional getWebSearchEngine(@NotNull ChatMessageContext chatMessageContext) { + if (chatMessageContext.getContext().equals(TAVILY_SEARCH_ACTION) && + SettingsStateService.getInstance().getTavilySearchKey() != null) { + return Optional.of(TavilyWebSearchEngine.builder() + .apiKey(SettingsStateService.getInstance().getTavilySearchKey()) + .build()); + } else if (SettingsStateService.getInstance().getGoogleSearchKey() != null && + chatMessageContext.getContext().equals(GOOGLE_SEARCH_ACTION)) { + return Optional.of(GoogleCustomWebSearchEngine.builder() + .apiKey(SettingsStateService.getInstance().getGoogleSearchKey()) + .csi(SettingsStateService.getInstance().getGoogleCSIKey()) + .build()); + } else { + return Optional.empty(); + } + } +} diff --git a/src/main/java/com/devoxx/genie/ui/DevoxxGenieSettingsManager.java b/src/main/java/com/devoxx/genie/ui/DevoxxGenieSettingsManager.java index 925dc018..a016c2c3 100644 --- a/src/main/java/com/devoxx/genie/ui/DevoxxGenieSettingsManager.java +++ b/src/main/java/com/devoxx/genie/ui/DevoxxGenieSettingsManager.java @@ -44,6 +44,10 @@ public class DevoxxGenieSettingsManager implements Configurable { private JPasswordField deepInfraKeyField; private JPasswordField geminiKeyField; + private JPasswordField tavilySearchKeyField; + private JPasswordField googleSearchKeyField; + private JPasswordField googleCSIKeyField; + private JFormattedTextField temperatureField; private JFormattedTextField topPField; @@ -52,6 +56,7 @@ public class DevoxxGenieSettingsManager implements Configurable { private JFormattedTextField chatMemorySizeField; private JCheckBox streamModeCheckBox; + private JCheckBox hideSearchCheckBox; private JCheckBox astModeCheckBox; private JCheckBox astParentClassCheckBox; @@ -102,6 +107,12 @@ public JComponent createComponent() { deepInfraKeyField = addFieldWithLabelPasswordAndLinkButton(settingsPanel, gbc, "DeepInfra API Key :", settings.getDeepInfraKey(), "https://deepinfra.com/dash/api_keys"); geminiKeyField = addFieldWithLabelPasswordAndLinkButton(settingsPanel, gbc, "Gemini API Key :", settings.getGeminiKey(), "https://aistudio.google.com/app/apikey"); + setTitle("Search Providers", settingsPanel, gbc); + hideSearchCheckBox = addCheckBoxWithLabel(settingsPanel, gbc, "Hide Search Providers", false, "", false); + tavilySearchKeyField = addFieldWithLabelPasswordAndLinkButton(settingsPanel, gbc, "Tavily Web Search API Key :", settings.getTavilySearchKey(), "https://app.tavily.com/home"); + googleSearchKeyField = addFieldWithLabelPasswordAndLinkButton(settingsPanel, gbc, "Google Web Search API Key :", settings.getGoogleSearchKey(), "https://developers.google.com/custom-search/docs/paid_element#api_key"); + googleCSIKeyField = addFieldWithLabelPasswordAndLinkButton(settingsPanel, gbc, "Google Custom Search Engine ID :", settings.getGoogleCSIKey(), "https://programmablesearchengine.google.com/controlpanel/create"); + setTitle("LLM Parameters", settingsPanel, gbc); chatMemorySizeField = addFormattedFieldWithLabel(settingsPanel, gbc, "Chat memory size:", settings.getTemperature()); @@ -127,6 +138,13 @@ public JComponent createComponent() { astReferenceFieldCheckBox.setEnabled(selected); }); + hideSearchCheckBox.addItemListener(e -> { + boolean selected = e.getStateChange() == ItemEvent.SELECTED; + tavilySearchKeyField.setEnabled(!selected); + googleSearchKeyField.setEnabled(!selected); + googleCSIKeyField.setEnabled(!selected); + }); + setTitle("Predefined Command Prompts", settingsPanel, gbc); testPromptField = addTextFieldWithLabel(settingsPanel, gbc, "Test prompt :", settings.getTestPrompt()); @@ -321,9 +339,9 @@ private void setTitle(String title, gbc.gridx++; panel.add(jPanel, gbc); } else { - panel.add(checkBox, gbc); - gbc.gridx++; panel.add(new JLabel(label), gbc); + gbc.gridx++; + panel.add(checkBox, gbc); } resetGbc(gbc); @@ -361,6 +379,12 @@ public boolean isModified() { isModified |= isFieldModified(groqKeyField, settings.getGroqKey()); isModified |= isFieldModified(deepInfraKeyField, settings.getDeepInfraKey()); isModified |= isFieldModified(geminiKeyField, settings.getGeminiKey()); + + isModified |= !settings.getHideSearchButtonsFlag().equals(hideSearchCheckBox.isSelected()); + isModified |= isFieldModified(tavilySearchKeyField, settings.getTavilySearchKey()); + isModified |= isFieldModified(googleSearchKeyField, settings.getGoogleSearchKey()); + isModified |= isFieldModified(googleCSIKeyField, settings.getGoogleCSIKey()); + isModified |= !settings.getStreamMode().equals(streamModeCheckBox.isSelected()); isModified |= !settings.getAstMode().equals(astModeCheckBox.isSelected()); isModified |= !settings.getAstParentClass().equals(astParentClassCheckBox.isSelected()); @@ -381,12 +405,22 @@ public void apply() { apiKeyModified |= updateSettingIfModified(groqKeyField, settings.getGroqKey(), settings::setGroqKey); apiKeyModified |= updateSettingIfModified(deepInfraKeyField, settings.getDeepInfraKey(), settings::setDeepInfraKey); apiKeyModified |= updateSettingIfModified(geminiKeyField, settings.getGeminiKey(), settings::setGeminiKey); + + apiKeyModified |= updateSettingIfModified(hideSearchCheckBox, settings.getHideSearchButtonsFlag(), value -> + settings.setHideSearchButtonsFlag(Boolean.parseBoolean(value)) + ); + apiKeyModified |= updateSettingIfModified(tavilySearchKeyField, settings.getTavilySearchKey(), settings::setTavilySearchKey); + apiKeyModified |= updateSettingIfModified(googleSearchKeyField, settings.getGoogleSearchKey(), settings::setGoogleSearchKey); + apiKeyModified |= updateSettingIfModified(googleCSIKeyField, settings.getGoogleCSIKey(), settings::setGoogleCSIKey); + if (apiKeyModified) { // Only notify the listener if an API key has changed, so we can refresh the LLM providers list in the UI notifySettingsChanged(); } + updateSettingIfModified(hideSearchCheckBox, settings.getHideSearchButtonsFlag(), value -> { + settings.setHideSearchButtonsFlag(Boolean.parseBoolean(value)); + }); - boolean chatMemoryModified = false; updateSettingIfModified(ollamaUrlField, settings.getOllamaModelUrl(), settings::setOllamaModelUrl); updateSettingIfModified(lmstudioUrlField, settings.getLmstudioModelUrl(), settings::setLmstudioModelUrl); updateSettingIfModified(gpt4allUrlField, settings.getGpt4allModelUrl(), settings::setGpt4allModelUrl); @@ -395,32 +429,26 @@ public void apply() { updateSettingIfModified(topPField, doubleConverter.toString(settings.getTopP()), value -> settings.setTopP(doubleConverter.fromString(value))); updateSettingIfModified(timeoutField, settings.getTimeout(), value -> settings.setTimeout(safeCastToInteger(value))); updateSettingIfModified(retryField, settings.getMaxRetries(), value -> settings.setMaxRetries(safeCastToInteger(value))); - chatMemoryModified = updateSettingIfModified(chatMemorySizeField, settings.getChatMemorySize(), value -> settings.setChatMemorySize(safeCastToInteger(value))); updateSettingIfModified(maxOutputTokensField, settings.getMaxOutputTokens(), settings::setMaxOutputTokens); updateSettingIfModified(testPromptField, settings.getTestPrompt(), settings::setTestPrompt); updateSettingIfModified(explainPromptField, settings.getExplainPrompt(), settings::setExplainPrompt); updateSettingIfModified(reviewPromptField, settings.getReviewPrompt(), settings::setReviewPrompt); updateSettingIfModified(customPromptField, settings.getCustomPrompt(), settings::setCustomPrompt); - updateSettingIfModified(openAiKeyField, settings.getOpenAIKey(), settings::setOpenAIKey); - updateSettingIfModified(mistralKeyField, settings.getMistralKey(), settings::setMistralKey); - updateSettingIfModified(anthropicKeyField, settings.getAnthropicKey(), settings::setAnthropicKey); - updateSettingIfModified(groqKeyField, settings.getGroqKey(), settings::setGroqKey); - updateSettingIfModified(deepInfraKeyField, settings.getDeepInfraKey(), settings::setDeepInfraKey); - updateSettingIfModified(geminiKeyField, settings.getGeminiKey(), settings::setGeminiKey); updateSettingIfModified(streamModeCheckBox, settings.getStreamMode(), value -> settings.setStreamMode(Boolean.parseBoolean(value))); + updateSettingIfModified(astModeCheckBox, settings.getAstMode(), value -> settings.setAstMode(Boolean.parseBoolean(value))); updateSettingIfModified(astParentClassCheckBox, settings.getAstParentClass(), value -> settings.setAstParentClass(Boolean.parseBoolean(value))); updateSettingIfModified(astReferenceClassesCheckBox, settings.getAstClassReference(), value -> settings.setAstClassReference(Boolean.parseBoolean(value))); updateSettingIfModified(astReferenceFieldCheckBox, settings.getAstFieldReference(), value -> settings.setAstFieldReference(Boolean.parseBoolean(value))); - if (chatMemoryModified) { + // Notify the listeners if the chat memory size has changed + if (updateSettingIfModified(chatMemorySizeField, settings.getChatMemorySize(), value -> settings.setChatMemorySize(safeCastToInteger(value)))) { notifyChatMemorySizeChangeListeners(); } } /** * Update the setting if the field value has changed - * * @param field the field * @param currentValue the current value * @param updateAction the update action @@ -447,7 +475,6 @@ public void notifyChatMemorySizeChangeListeners() { /** * Extract the string value from the field - * * @param field the field * @return the string value */ @@ -475,13 +502,22 @@ public void reset() { ollamaUrlField.setText(settingsState.getOllamaModelUrl()); lmstudioUrlField.setText(settingsState.getLmstudioModelUrl()); gpt4allUrlField.setText(settingsState.getGpt4allModelUrl()); + janUrlField.setText(settingsState.getJanModelUrl()); + chatMemorySizeField.setText(String.valueOf(settingsState.getChatMemorySize())); testPromptField.setText(settingsState.getTestPrompt()); explainPromptField.setText(settingsState.getExplainPrompt()); reviewPromptField.setText(settingsState.getReviewPrompt()); customPromptField.setText(settingsState.getCustomPrompt()); maxOutputTokensField.setText(settingsState.getMaxOutputTokens()); + streamModeCheckBox.setSelected(settingsState.getStreamMode()); + astReferenceFieldCheckBox.setSelected(settingsState.getAstFieldReference()); + astParentClassCheckBox.setSelected(settingsState.getAstParentClass()); + astReferenceClassesCheckBox.setSelected(settingsState.getAstClassReference()); + + hideSearchCheckBox.setSelected(settingsState.getHideSearchButtonsFlag()); + setValue(temperatureField, settingsState.getTemperature()); setValue(topPField, settingsState.getTopP()); setValue(timeoutField, settingsState.getTimeout()); @@ -491,7 +527,6 @@ public void reset() { /** * Set the value of the field - * * @param field the field * @param value the value */ diff --git a/src/main/java/com/devoxx/genie/ui/DevoxxGenieToolWindowContent.java b/src/main/java/com/devoxx/genie/ui/DevoxxGenieToolWindowContent.java index 35b11516..ca2d006f 100644 --- a/src/main/java/com/devoxx/genie/ui/DevoxxGenieToolWindowContent.java +++ b/src/main/java/com/devoxx/genie/ui/DevoxxGenieToolWindowContent.java @@ -3,13 +3,11 @@ import com.devoxx.genie.chatmodel.ChatModelFactory; import com.devoxx.genie.chatmodel.ChatModelFactoryProvider; import com.devoxx.genie.chatmodel.ChatModelProvider; +import com.devoxx.genie.model.Constant; import com.devoxx.genie.model.enumarations.ModelProvider; import com.devoxx.genie.model.request.ChatMessageContext; import com.devoxx.genie.model.request.EditorInfo; -import com.devoxx.genie.service.ChatMemoryService; -import com.devoxx.genie.service.ChatPromptExecutor; -import com.devoxx.genie.service.FileListManager; -import com.devoxx.genie.service.SettingsStateService; +import com.devoxx.genie.service.*; import com.devoxx.genie.ui.component.ContextPopupMenu; import com.devoxx.genie.ui.component.JHoverButton; import com.devoxx.genie.ui.component.PromptInputArea; @@ -20,11 +18,7 @@ import com.devoxx.genie.ui.panel.PromptOutputPanel; import com.devoxx.genie.ui.util.EditorUtil; import com.devoxx.genie.ui.util.NotificationUtil; -import com.intellij.openapi.application.ApplicationManager; -import com.intellij.openapi.diagnostic.Logger; -import com.intellij.openapi.editor.Document; import com.intellij.openapi.editor.Editor; -import com.intellij.openapi.fileEditor.FileDocumentManager; import com.intellij.openapi.project.Project; import com.intellij.openapi.ui.ComboBox; import com.intellij.openapi.ui.Splitter; @@ -36,6 +30,7 @@ import dev.langchain4j.data.message.UserMessage; import lombok.Getter; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; import javax.swing.*; import java.awt.*; @@ -43,9 +38,10 @@ import java.util.List; import java.util.ResourceBundle; -import static com.devoxx.genie.action.AddSnippetAction.SELECTED_TEXT_KEY; import static com.devoxx.genie.chatmodel.LLMProviderConstant.getLLMProviders; +import static com.devoxx.genie.model.Constant.*; import static com.devoxx.genie.ui.util.DevoxxGenieIcons.*; +import static com.devoxx.genie.ui.util.SettingsDialogUtil.showSettingsDialog; import static javax.swing.SwingUtilities.invokeLater; /** @@ -53,12 +49,8 @@ */ public class DevoxxGenieToolWindowContent implements SettingsChangeListener, ConversationStarter { - private final Logger LOG = Logger.getInstance(DevoxxGenieToolWindowContent.class); - private final Project project; - private final ResourceBundle resourceBundle = ResourceBundle.getBundle("messages"); - public static final String COMBO_BOX_CHANGED = "comboBoxChanged"; - + private final ResourceBundle resourceBundle = ResourceBundle.getBundle(MESSAGES); private final ChatModelProvider chatModelProvider = new ChatModelProvider(); @Getter @@ -66,19 +58,22 @@ public class DevoxxGenieToolWindowContent implements SettingsChangeListener, Con private final ComboBox llmProvidersComboBox = new ComboBox<>(); private final ComboBox modelNameComboBox = new ComboBox<>(); + private ConversationPanel conversationPanel; private PromptInputArea promptInputComponent; private PromptOutputPanel promptOutputPanel; private PromptContextFileListPanel promptContextFileListPanel; private final JButton submitBtn = new JHoverButton(SubmitIcon, true); + private final JButton tavilySearchBtn = new JHoverButton(WebSearchIcon, true); + private final JButton googleSearchBtn = new JHoverButton(GoogleIcon, true); private final JButton addFileBtn = new JHoverButton(AddFileIcon, true); private final ChatPromptExecutor chatPromptExecutor; private final SettingsStateService settingsState; - private ConversationPanel conversationPanel; - private boolean isInitializationComplete = false; private final EditorFileButtonManager editorFileButtonManager; + private boolean isInitializationComplete = false; + /** * The Devoxx Genie Tool Window Content constructor. * @@ -94,6 +89,7 @@ public DevoxxGenieToolWindowContent(@NotNull ToolWindow toolWindow) { setupUI(); setLastSelectedProvider(); + configureSearchButtonsVisibility(); isInitializationComplete = true; } @@ -142,7 +138,6 @@ private void setupUI() { /** * Create the splitter. - * * @return the splitter */ private @NotNull Splitter createSplitter() { @@ -154,11 +149,35 @@ private void setupUI() { } /** - * Refresh the LLM providers dropdown because the Settings have been changed. + * Check if web search is enabled. + * @return true if web search is enabled + */ + private boolean isWebSearchEnabled() { + return !SettingsStateService.getInstance().getTavilySearchKey().isEmpty() || + !SettingsStateService.getInstance().getGoogleSearchKey().isEmpty(); + } + + /** + * Refresh the UI elements because the settings have changed. */ public void settingsChanged() { llmProvidersComboBox.removeAllItems(); addLLMProvidersToComboBox(); + configureSearchButtonsVisibility(); + } + + /** + * Set the search buttons visibility based on settings. + */ + private void configureSearchButtonsVisibility() { + if (settingsState.getHideSearchButtonsFlag()) { + tavilySearchBtn.setVisible(false); + googleSearchBtn.setVisible(false); + } else { + tavilySearchBtn.setVisible(!SettingsStateService.getInstance().getTavilySearchKey().isEmpty()); + googleSearchBtn.setVisible(!SettingsStateService.getInstance().getGoogleSearchKey().isEmpty() && + !SettingsStateService.getInstance().getGoogleCSIKey().isEmpty()); + } } /** @@ -173,7 +192,6 @@ private void addLLMProvidersToComboBox() { /** * Create the LLM and model name selection panel. - * * @return the selection panel */ @NotNull @@ -189,7 +207,6 @@ private JPanel createSelectionPanel() { /** * Create the tool panel. - * * @return the tool panel */ private @NotNull JPanel createToolPanel() { @@ -203,7 +220,6 @@ private JPanel createSelectionPanel() { /** * Create the LLM provider panel. - * * @return the provider panel */ private @NotNull JPanel createProviderPanel() { @@ -215,46 +231,52 @@ private JPanel createSelectionPanel() { } /** - * Create the chat input panel. - * - * @return the input panel - */ - @NotNull - private JPanel createInputPanel() { - JPanel buttonPanel = createButtonPanel(); - return createSubmitPanel(buttonPanel); - } - - /** - * Create the Submit and add File button Panel. - * + * Create the Action button panel with Submit, the Web Search and Add file buttons. * @return the button panel */ - private @NotNull JPanel createButtonPanel() { - addFileBtn.setToolTipText("Add file(s) to prompt context"); - addFileBtn.addActionListener(this::selectFilesForPromptContext); + private @NotNull JPanel createActionButtonsPanel() { - submitBtn.setToolTipText("Submit the prompt"); + JPanel actionButtonsPanel = new JPanel(new BorderLayout()); + + submitBtn.setToolTipText(SUBMIT_THE_PROMPT); + submitBtn.setActionCommand(Constant.SUBMIT_ACTION); submitBtn.addActionListener(this::onSubmitPrompt); + actionButtonsPanel.add(submitBtn, BorderLayout.WEST); + + JPanel searchPanel = new JPanel(new FlowLayout()); + createSearchButton(searchPanel, tavilySearchBtn, TAVILY_SEARCH_ACTION, SEARCH_THE_WEB_WITH_TAVILY_FOR_AN_ANSWER); + createSearchButton(searchPanel, googleSearchBtn, GOOGLE_SEARCH_ACTION, SEARCH_GOOGLE_FOR_AN_ANSWER); + actionButtonsPanel.add(searchPanel, BorderLayout.CENTER); - JPanel buttonPanel = new JPanel(new BorderLayout()); - buttonPanel.add(submitBtn, BorderLayout.WEST); - buttonPanel.add(addFileBtn, BorderLayout.EAST); - return buttonPanel; + addFileBtn.setToolTipText(ADD_FILE_S_TO_PROMPT_CONTEXT); + addFileBtn.addActionListener(this::selectFilesForPromptContext); + + actionButtonsPanel.add(addFileBtn, BorderLayout.EAST); + + return actionButtonsPanel; + } + + private void createSearchButton(@NotNull JPanel panel, + @NotNull JButton searchBtn, + String searchAction, + String tooltipText) { + searchBtn.setMaximumSize(new Dimension(30, 30)); + searchBtn.setActionCommand(searchAction); + searchBtn.setToolTipText(tooltipText); + searchBtn.addActionListener(this::onSubmitPrompt); + panel.add(searchBtn); } /** * Create the Submit panel. - * - * @param buttonPanel the button panel * @return the Submit panel */ - private @NotNull JPanel createSubmitPanel(JPanel buttonPanel) { + private @NotNull JPanel createInputPanel() { JPanel submitPanel = new JPanel(new BorderLayout()); submitPanel.setMinimumSize(new Dimension(Integer.MAX_VALUE, 100)); submitPanel.add(promptContextFileListPanel, BorderLayout.NORTH); submitPanel.add(new JBScrollPane(promptInputComponent), BorderLayout.CENTER); - submitPanel.add(new JBScrollPane(buttonPanel), BorderLayout.SOUTH); + submitPanel.add(createActionButtonsPanel(), BorderLayout.SOUTH); return submitPanel; } @@ -294,38 +316,74 @@ private void selectFilesForPromptContext(ActionEvent e) { /** * Submit the user prompt. */ - private void onSubmitPrompt(ActionEvent e) { - String userPromptText = promptInputComponent.getText(); - if (userPromptText.isEmpty()) { - return; - } + private void onSubmitPrompt(ActionEvent actionEvent) { + String userPromptText = isUserPromptProvided(); + if (userPromptText == null) return; + if (isWebSearchTriggeredAndConfigured(actionEvent)) return; + + disableSubmitBtn(); + + ChatMessageContext chatMessageContext = + createChatMessageContext(actionEvent, userPromptText, editorFileButtonManager.getSelectedTextEditor()); + + disableButtons(); + + chatPromptExecutor.updatePromptWithCommandIfPresent(chatMessageContext, promptOutputPanel); + chatPromptExecutor.executePrompt(chatMessageContext, promptOutputPanel, this::enableButtons); + } + + /** + * Disable the Submit button. + */ + private void disableSubmitBtn() { invokeLater(() -> { if (SettingsStateService.getInstance().getStreamMode()) { submitBtn.setEnabled(false); } submitBtn.setIcon(StopIcon); - submitBtn.setToolTipText("Prompt is running, please be patient..."); + submitBtn.setToolTipText(PROMPT_IS_RUNNING_PLEASE_BE_PATIENT); }); + } - ChatMessageContext chatMessageContext = - createChatMessageContext(userPromptText, editorFileButtonManager.getSelectedTextEditor()); - - disableButtons(); - - chatPromptExecutor.updatePromptWithCommandIfPresent(chatMessageContext, promptOutputPanel); + /** + * Check if web search is triggered and configured, if not show Settings page. + * @param actionEvent the action event + * @return true if the web search is triggered and configured + */ + private boolean isWebSearchTriggeredAndConfigured(@NotNull ActionEvent actionEvent) { + if (actionEvent.getActionCommand().toLowerCase().contains("search") && !isWebSearchEnabled()) { + SwingUtilities.invokeLater(() -> + NotificationUtil.sendNotification(project, "No Search API keys found, please add one in the settings.") + ); + showSettingsDialog(project); + return true; + } + return false; + } - chatPromptExecutor.executePrompt(chatMessageContext, promptOutputPanel, this::enableButtons); + /** + * Check if the user prompt is provided. + * @return the user prompt text + */ + private @Nullable String isUserPromptProvided() { + String userPromptText = promptInputComponent.getText(); + if (userPromptText.isEmpty()) { + NotificationUtil.sendNotification(project, "Please enter a prompt."); + return null; + } + return userPromptText; } /** * Get the chat message context. - * + * @param actionEvent the action event * @param userPrompt the user prompt * @param editor the editor * @return the prompt context with language and text */ - private @NotNull ChatMessageContext createChatMessageContext(String userPrompt, + private @NotNull ChatMessageContext createChatMessageContext(ActionEvent actionEvent, + String userPrompt, Editor editor) { ChatMessageContext chatMessageContext = new ChatMessageContext(); chatMessageContext.setProject(project); @@ -335,7 +393,7 @@ private void onSubmitPrompt(ActionEvent e) { chatMessageContext.setLlmProvider((String) llmProvidersComboBox.getSelectedItem()); chatMessageContext.setModelName((String) modelNameComboBox.getSelectedItem()); - if (settingsState.getStreamMode()) { + if (settingsState.getStreamMode() && actionEvent.getActionCommand().equals(Constant.SUBMIT_ACTION)) { chatMessageContext.setStreamingChatLanguageModel(chatModelProvider.getStreamingChatLanguageModel(chatMessageContext)); } else { chatMessageContext.setChatLanguageModel(chatModelProvider.getChatLanguageModel(chatMessageContext)); @@ -343,14 +401,17 @@ private void onSubmitPrompt(ActionEvent e) { setChatTimeout(chatMessageContext); - addSelectedCode(userPrompt, editor, chatMessageContext); + if (actionEvent.getActionCommand().equals(Constant.SUBMIT_ACTION)) { + addSelectedCode(userPrompt, editor, chatMessageContext); + } else { + chatMessageContext.setContext(actionEvent.getActionCommand()); + } return chatMessageContext; } /** * Add the selected code to the chat message context. - * * @param userPrompt the user prompt * @param editor the editor * @param chatMessageContext the chat message context @@ -369,7 +430,6 @@ private void addSelectedCode(String userPrompt, /** * Create the editor info. - * * @param editor the editor * @return the editor info */ @@ -387,7 +447,6 @@ private void addSelectedCode(String userPrompt, /** * Set the timeout for the chat message context. - * * @param chatMessageContext the chat message context */ private void setChatTimeout(ChatMessageContext chatMessageContext) { @@ -401,7 +460,6 @@ private void setChatTimeout(ChatMessageContext chatMessageContext) { /** * Get the prompt context from the selected files. - * * @param chatMessageContext the chat message context * @param userPrompt the user prompt * @param files the files @@ -410,39 +468,12 @@ private void addSelectedFiles(@NotNull ChatMessageContext chatMessageContext, String userPrompt, List files) { chatMessageContext.setEditorInfo(new EditorInfo(files)); - chatMessageContext.setContext(getUserPromptWithContext(userPrompt, files)); - } - /** - * Get user prompt with context. - * TODO move this to a dedicated class - * - * @param userPrompt the user prompt - * @param files the files - * @return the user prompt with context - */ - private @NotNull String getUserPromptWithContext(String userPrompt, - @NotNull List files) { - StringBuilder userPromptContext = new StringBuilder(); - FileDocumentManager fileDocumentManager = FileDocumentManager.getInstance(); - files.forEach(file -> ApplicationManager.getApplication().runReadAction(() -> { - if (file.getFileType().getName().equals("UNKNOWN")) { - userPromptContext.append("Filename: ").append(file.getName()).append("\n"); - userPromptContext.append("Code Snippet: ").append(file.getUserData(SELECTED_TEXT_KEY)).append("\n"); - } else { - Document document = fileDocumentManager.getDocument(file); - if (document != null) { - userPromptContext.append("Filename: ").append(file.getName()).append("\n"); - String content = document.getText(); - userPromptContext.append(content).append("\n"); - } else { - NotificationUtil.sendNotification(project, "Error reading file: " + file.getName()); - } - } - })); + String userPromptWithContext = MessageCreationService + .getInstance() + .createUserPromptWithContext(chatMessageContext.getProject(), userPrompt, files); - userPromptContext.append(userPrompt); - return userPromptContext.toString(); + chatMessageContext.setContext(userPromptWithContext); } /** @@ -453,11 +484,12 @@ private void disableButtons() { } /** - * Enable the prompt input component and reset submit icon. + * Enable the prompt input component and reset the Submit button icon. */ private void enableButtons() { submitBtn.setIcon(SubmitIcon); submitBtn.setEnabled(true); + submitBtn.setToolTipText(SUBMIT_THE_PROMPT); promptInputComponent.setEnabled(true); } @@ -465,7 +497,7 @@ private void enableButtons() { * Process the model name selection. */ private void processModelNameSelection(@NotNull ActionEvent e) { - if (e.getActionCommand().equals(COMBO_BOX_CHANGED)) { + if (e.getActionCommand().equals(Constant.COMBO_BOX_CHANGED)) { JComboBox comboBox = (JComboBox) e.getSource(); String selectedModel = (String) comboBox.getSelectedItem(); if (selectedModel != null) { @@ -479,7 +511,7 @@ private void processModelNameSelection(@NotNull ActionEvent e) { * Set the model provider and update the model names. */ private void handleModelProviderSelectionChange(@NotNull ActionEvent e) { - if (!e.getActionCommand().equals(COMBO_BOX_CHANGED) || !isInitializationComplete) return; + if (!e.getActionCommand().equals(Constant.COMBO_BOX_CHANGED) || !isInitializationComplete) return; JComboBox comboBox = (JComboBox) e.getSource(); @@ -487,7 +519,7 @@ private void handleModelProviderSelectionChange(@NotNull ActionEvent e) { if (selectedLLMProvider == null) return; settingsState.setLastSelectedProvider(selectedLLMProvider); - ModelProvider provider = ModelProvider.valueOf(selectedLLMProvider); + ModelProvider provider = ModelProvider.fromString(selectedLLMProvider); updateModelNamesComboBox(provider); } @@ -517,7 +549,6 @@ private void updateModelNamesComboBox(ModelProvider provider) { /** * Populate the model names. - * * @param chatModelFactory the chat model factory */ private void populateModelNames(@NotNull ChatModelFactory chatModelFactory) { diff --git a/src/main/java/com/devoxx/genie/ui/panel/ChatResponsePanel.java b/src/main/java/com/devoxx/genie/ui/panel/ChatResponsePanel.java index 431b36b5..b2ce1f7a 100644 --- a/src/main/java/com/devoxx/genie/ui/panel/ChatResponsePanel.java +++ b/src/main/java/com/devoxx/genie/ui/panel/ChatResponsePanel.java @@ -21,7 +21,6 @@ public class ChatResponsePanel extends BackgroundPanel { /** * Create a new chat response panel. - * * @param chatMessageContext the chat message context */ public ChatResponsePanel(@NotNull ChatMessageContext chatMessageContext) { diff --git a/src/main/java/com/devoxx/genie/ui/panel/ConversationPanel.java b/src/main/java/com/devoxx/genie/ui/panel/ConversationPanel.java index 9af18dbb..23c85a5d 100644 --- a/src/main/java/com/devoxx/genie/ui/panel/ConversationPanel.java +++ b/src/main/java/com/devoxx/genie/ui/panel/ConversationPanel.java @@ -2,6 +2,7 @@ import com.devoxx.genie.ui.ConversationStarter; import com.devoxx.genie.ui.component.JHoverButton; +import com.devoxx.genie.ui.util.SettingsDialogUtil; import com.intellij.openapi.options.ShowSettingsUtil; import com.intellij.openapi.project.Project; import com.intellij.ui.JBColor; @@ -19,7 +20,7 @@ public class ConversationPanel extends JPanel { private final JButton newConversationBtn = new JHoverButton(PlusIcon, true); - private final JButton configBtn = new JHoverButton(CogIcon, true); + private final JButton settingsBtn = new JHoverButton(CogIcon, true); private final Project project; private final ConversationStarter conversationStarter; @@ -53,13 +54,12 @@ public ConversationPanel(Project project, ConversationStarter conversationStarte * Setup the conversation buttons. */ private void setupConversationButtons() { - newConversationBtn.setPreferredSize(new Dimension(25, 30)); - configBtn.setPreferredSize(new Dimension(25, 30)); + settingsBtn.setPreferredSize(new Dimension(25, 30)); + settingsBtn.setToolTipText("Plugin settings"); + settingsBtn.addActionListener(e -> SettingsDialogUtil.showSettingsDialog(project)); + newConversationBtn.setPreferredSize(new Dimension(25, 30)); newConversationBtn.setToolTipText("Start a new conversation"); - configBtn.setToolTipText("Plugin settings"); - - configBtn.addActionListener(e -> showSettingsDialog()); newConversationBtn.addActionListener(e -> conversationStarter.startNewConversation()); } @@ -71,7 +71,7 @@ private void setupConversationButtons() { private @NotNull JPanel createButtonPanel() { JPanel conversationButtonPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT, 0, 0)); conversationButtonPanel.add(newConversationBtn); - conversationButtonPanel.add(configBtn); + conversationButtonPanel.add(settingsBtn); conversationButtonPanel.setPreferredSize(new Dimension(60, 30)); conversationButtonPanel.setMinimumSize(new Dimension(60, 30)); return conversationButtonPanel; @@ -83,11 +83,4 @@ private void setupConversationButtons() { public void updateNewConversationLabel() { newConversationLabel.setText("New conversation " + getCurrentTimestamp()); } - - /** - * Show the settings dialog. - */ - private void showSettingsDialog() { - ShowSettingsUtil.getInstance().showSettingsDialog(project, "Devoxx Genie Settings"); - } } diff --git a/src/main/java/com/devoxx/genie/ui/panel/PromptOutputPanel.java b/src/main/java/com/devoxx/genie/ui/panel/PromptOutputPanel.java index 54182ad9..48b9f580 100644 --- a/src/main/java/com/devoxx/genie/ui/panel/PromptOutputPanel.java +++ b/src/main/java/com/devoxx/genie/ui/panel/PromptOutputPanel.java @@ -81,7 +81,6 @@ private void addFiller(String name) { /** * Add a user prompt to the panel. - * * @param chatMessageContext the prompt context */ public void addUserPrompt(ChatMessageContext chatMessageContext) { @@ -96,7 +95,6 @@ public void addUserPrompt(ChatMessageContext chatMessageContext) { addFiller(chatMessageContext.getName()); container.add(userPromptPanel); - // moveToBottom(); } /** @@ -108,7 +106,6 @@ public void addChatResponse(@NotNull ChatMessageContext chatMessageContext) { waitingPanel.hideMsg(); addFiller(chatMessageContext.getName()); container.add(new ChatResponsePanel(chatMessageContext)); - // moveToBottom(); } /** @@ -118,12 +115,10 @@ public void addChatResponse(@NotNull ChatMessageContext chatMessageContext) { */ public void addStreamResponse(ChatStreamingResponsePanel chatResponseStreamingPanel) { container.add(chatResponseStreamingPanel); - // moveToBottom(); } public void addStreamFileReferencesResponse(ExpandablePanel fileListPanel) { container.add(fileListPanel); - // moveToBottom(); } /** diff --git a/src/main/java/com/devoxx/genie/ui/util/DevoxxGenieIcons.java b/src/main/java/com/devoxx/genie/ui/util/DevoxxGenieIcons.java index a81465f9..1972d210 100644 --- a/src/main/java/com/devoxx/genie/ui/util/DevoxxGenieIcons.java +++ b/src/main/java/com/devoxx/genie/ui/util/DevoxxGenieIcons.java @@ -10,6 +10,8 @@ public final class DevoxxGenieIcons { public static final Icon PlusIcon = load("/icons/plus.svg"); public static final Icon AddFileIcon = load("/icons/addNewFile.svg"); public static final Icon SubmitIcon = load("/icons/paperPlane.svg"); + public static final Icon WebSearchIcon = load("/icons/web.svg"); + public static final Icon GoogleIcon = load("/icons/google-small.svg"); public static final Icon ArrowExpand = load("/icons/arrowExpand.svg"); public static final Icon ArrowExpanded = load("/icons/arrowExpanded.svg"); public static final Icon CloseSmalllIcon = load("/icons/closeSmall_dark.svg"); diff --git a/src/main/java/com/devoxx/genie/ui/util/SettingsDialogUtil.java b/src/main/java/com/devoxx/genie/ui/util/SettingsDialogUtil.java new file mode 100644 index 00000000..97423a93 --- /dev/null +++ b/src/main/java/com/devoxx/genie/ui/util/SettingsDialogUtil.java @@ -0,0 +1,14 @@ +package com.devoxx.genie.ui.util; + +import com.intellij.openapi.options.ShowSettingsUtil; +import com.intellij.openapi.project.Project; + +public class SettingsDialogUtil { + + /** + * Show the settings dialog. + */ + public static void showSettingsDialog(Project project) { + ShowSettingsUtil.getInstance().showSettingsDialog(project, "Devoxx Genie Settings"); + } +} diff --git a/src/main/java/com/devoxx/genie/ui/util/WelcomeUtil.java b/src/main/java/com/devoxx/genie/ui/util/WelcomeUtil.java index f39c4642..38e5f2f1 100644 --- a/src/main/java/com/devoxx/genie/ui/util/WelcomeUtil.java +++ b/src/main/java/com/devoxx/genie/ui/util/WelcomeUtil.java @@ -13,6 +13,12 @@ public static String getWelcomeText(ResourceBundle resourceBundle) { font-family: 'Source Code Pro', monospace; font-size: 14pt; margin: 5px; } + ul { + list-style-type: none; + } + li { + margin-bottom: 10px; + } @@ -22,11 +28,10 @@ public static String getWelcomeText(ResourceBundle resourceBundle) {

New features 🚀

Enable these new features in the settings page.
    -
  • Streaming responses (beta): See each token as it's received from the LLM in real-time.
  • -
    -
  • Abstract Syntax Tree (AST) context: Automatically include parent class and class/field references in the prompt for better code analysis. Ensure the LLM has a large enough context window.
  • -
    -
  • Chat Memory Size: Set the size of your chat memory, by default its set to a total of 10 messages (system + user msgs).
  • +
  • 🔍Web Search: Search the web for a given query using Google or Tavily
  • +
  • 🌊Streaming responses (beta): See each token as it's received from the LLM in real-time
  • +
  • 🧐Abstract Syntax Tree (AST) context: Automatically include parent class and class/field references in the prompt for better code analysis. Ensure the LLM has a large enough context window
  • +
  • 💬Chat Memory Size: Set the size of your chat memory, by default its set to a total of 10 messages (system + user msgs)

%s

    diff --git a/src/main/resources/META-INF/plugin.xml b/src/main/resources/META-INF/plugin.xml index 025ddffd..185c2a7b 100644 --- a/src/main/resources/META-INF/plugin.xml +++ b/src/main/resources/META-INF/plugin.xml @@ -35,6 +35,7 @@

    v0.1.15

    • Fix #82: Wrap text to new line for streaming output.
    • +
    • Feat #85: Support Web Search

    v0.1.14

      @@ -193,6 +194,7 @@ + diff --git a/src/main/resources/icons/google-small.svg b/src/main/resources/icons/google-small.svg new file mode 100644 index 00000000..981ade7b --- /dev/null +++ b/src/main/resources/icons/google-small.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/src/main/resources/icons/web.svg b/src/main/resources/icons/web.svg new file mode 100644 index 00000000..9ed5e6c4 --- /dev/null +++ b/src/main/resources/icons/web.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/src/main/resources/icons/web_dark.svg b/src/main/resources/icons/web_dark.svg new file mode 100644 index 00000000..889c74d7 --- /dev/null +++ b/src/main/resources/icons/web_dark.svg @@ -0,0 +1,7 @@ + + + + + + +