From f90df32af8ba44fe56b30be3fb77eb404757a026 Mon Sep 17 00:00:00 2001 From: samir Date: Wed, 18 Dec 2024 10:06:50 +0100 Subject: [PATCH 1/2] fix(issue-243): rollbacking modifications --- .../genie/ui/panel/ChatResponsePanel.java | 231 ++++++++++++++++-- .../ui/panel/ChatStreamingResponsePanel.java | 1 - .../ResponseHeaderPanel.java | 2 +- .../ui/panel/chatresponse/FileListPanel.java | 19 -- .../MetricExecutionInfoPanel.java | 65 ----- .../chatresponse/ResponseContentPanel.java | 71 ------ .../SemanticSearchReferencesPanel.java | 22 -- 7 files changed, 211 insertions(+), 200 deletions(-) rename src/main/java/com/devoxx/genie/ui/panel/{chatresponse => }/ResponseHeaderPanel.java (98%) delete mode 100644 src/main/java/com/devoxx/genie/ui/panel/chatresponse/FileListPanel.java delete mode 100644 src/main/java/com/devoxx/genie/ui/panel/chatresponse/MetricExecutionInfoPanel.java delete mode 100644 src/main/java/com/devoxx/genie/ui/panel/chatresponse/ResponseContentPanel.java delete mode 100644 src/main/java/com/devoxx/genie/ui/panel/chatresponse/SemanticSearchReferencesPanel.java 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 f08df12e..3f80061c 100644 --- a/src/main/java/com/devoxx/genie/ui/panel/ChatResponsePanel.java +++ b/src/main/java/com/devoxx/genie/ui/panel/ChatResponsePanel.java @@ -1,46 +1,235 @@ package com.devoxx.genie.ui.panel; +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.model.request.SemanticFile; import com.devoxx.genie.service.FileListManager; -import com.devoxx.genie.ui.panel.chatresponse.*; +import com.devoxx.genie.service.ProjectContentService; +import com.devoxx.genie.service.gitdiff.GitMergeService; +import com.devoxx.genie.ui.component.ExpandablePanel; +import com.devoxx.genie.ui.processor.NodeProcessorFactory; +import com.devoxx.genie.ui.settings.DevoxxGenieStateService; +import com.devoxx.genie.util.DefaultLLMSettingsUtil; +import com.intellij.openapi.vfs.VirtualFile; +import com.intellij.ui.JBColor; +import com.knuddels.jtokkit.api.Encoding; +import dev.langchain4j.model.output.TokenUsage; +import org.commonmark.node.Block; +import org.commonmark.node.FencedCodeBlock; +import org.commonmark.node.IndentedCodeBlock; +import org.commonmark.node.Node; +import org.commonmark.parser.Parser; import org.jetbrains.annotations.NotNull; import javax.swing.*; import java.awt.*; +import java.text.NumberFormat; +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; + +import static com.devoxx.genie.ui.util.DevoxxGenieFontsUtil.SourceCodeProFontPlan14; public class ChatResponsePanel extends BackgroundPanel { private final transient ChatMessageContext chatMessageContext; + /** + * Create a new chat response panel. + * + * @param chatMessageContext the chat message context + */ public ChatResponsePanel(@NotNull ChatMessageContext chatMessageContext) { super(chatMessageContext.getId()); + setLayout(new BoxLayout(this, BoxLayout.Y_AXIS)); + this.chatMessageContext = chatMessageContext; - setLayout(new GridBagLayout()); - buildResponsePanel(); + + add(new ResponseHeaderPanel(chatMessageContext)); + addResponsePane(chatMessageContext); + } + + /** + * Get the response pane with rendered HTML. + * + * @param chatMessageContext the chat message context + */ + private void addResponsePane(@NotNull ChatMessageContext chatMessageContext) { + String markDownResponse = chatMessageContext.getAiMessage().text(); + Node document = Parser.builder().build().parse(markDownResponse); + + DevoxxGenieStateService stateService = DevoxxGenieStateService.getInstance(); + + // If git diff is activated, try to extract code blocks and show diff + if (Boolean.TRUE.equals(stateService.getGitDiffActivated())) { + processGitDiff(chatMessageContext, document); + } + + addDocumentNodesToPanel(document); + + // Add regular files panel + if (chatMessageContext.hasFiles()) { + java.util.List files = FileListManager.getInstance().getFiles(); + ExpandablePanel fileListPanel = new ExpandablePanel(chatMessageContext, files); + add(fileListPanel); + } + + // Add semantic references panel + addSemanticSearchReferences(chatMessageContext); + + if (Boolean.TRUE.equals(DevoxxGenieStateService.getInstance().getShowExecutionTime())) { + // Add execution time, token usage and cost information + addMetricExecutionInfo(chatMessageContext); + } + } + + private void addSemanticSearchReferences(@NotNull ChatMessageContext chatMessageContext) { + List semanticReferences = chatMessageContext.getSemanticReferences(); + if (semanticReferences != null && !semanticReferences.isEmpty()) { + ExpandablePanel semanticPanel = new ExpandablePanel(chatMessageContext.getProject(), semanticReferences); + semanticPanel.setName(chatMessageContext.getId() + "_semantic"); + add(semanticPanel); + } + } + + private void addMetricExecutionInfo(@NotNull ChatMessageContext chatMessageContext) { + JPanel metricExecutionInfoPanel = new JPanel(new FlowLayout(FlowLayout.LEFT)); + metricExecutionInfoPanel.setOpaque(false); + + String metricInfoLabel = String.format("ϟ %.2fs", chatMessageContext.getExecutionTimeMs() / 1000.0); + + dev.langchain4j.model.output.TokenUsage tokenUsage = chatMessageContext.getTokenUsage(); + if (tokenUsage != null) { + + String cost = ""; + if (DefaultLLMSettingsUtil.isApiKeyBasedProvider(chatMessageContext.getLanguageModel().getProvider())) { + cost = String.format("- %.5f $", chatMessageContext.getCost()); + } + + tokenUsage = calcOllamaInputTokenCount(chatMessageContext, tokenUsage); + + NumberFormat numberFormat = NumberFormat.getNumberInstance(Locale.getDefault()); + String formattedInputTokens = numberFormat.format(tokenUsage.inputTokenCount()); + String formattedOutputTokens = numberFormat.format(tokenUsage.outputTokenCount()); + + metricInfoLabel += String.format(" - Tokens ↑ %s ↓️ %s %s", formattedInputTokens, formattedOutputTokens, cost); + } + + JLabel tokenLabel = new JLabel(metricInfoLabel); + + tokenLabel.setForeground(JBColor.GRAY); + tokenLabel.setFont(tokenLabel.getFont().deriveFont(12f)); + + metricExecutionInfoPanel.add(tokenLabel); + add(metricExecutionInfoPanel); + } + + /** + * Process the git diff. + * @param chatMessageContext the chat message context + * @param document the document + */ + private void processGitDiff(@NotNull ChatMessageContext chatMessageContext, @NotNull Node document) { + // Get original file info + EditorInfo editorInfo = chatMessageContext.getEditorInfo(); + if (editorInfo == null) { + return; + } + + if (editorInfo.getSelectedFiles() != null && !editorInfo.getSelectedFiles().isEmpty()) { + List files = editorInfo.getSelectedFiles(); + List modifiedContents = new ArrayList<>(); + + // Collect modified contents from code blocks + Node node = document.getFirstChild(); + while (node != null) { + if (node instanceof FencedCodeBlock codeBlock) { + modifiedContents.add(codeBlock.getLiteral()); + } + node = node.getNext(); + } + + GitMergeService.getInstance().showDiffView( + chatMessageContext.getProject(), + files.get(0), + modifiedContents.get(0)); + } } - private void buildResponsePanel() { - GridBagConstraints gbc = new GridBagConstraints(); - gbc.gridx = 0; - gbc.gridy = 0; - gbc.weightx = 1; // full width components - gbc.fill = GridBagConstraints.HORIZONTAL; // Fill horizontally - gbc.anchor = GridBagConstraints.WEST; // Anchor to the west (left) + /** + * Ollama does not count the input context tokens in the token usage, this method fixes this. + * + * @param chatMessageContext the chat message context + * @param tokenUsage the token usage + * @return the updated token usage + */ + private static TokenUsage calcOllamaInputTokenCount(@NotNull ChatMessageContext chatMessageContext, TokenUsage tokenUsage) { + if (chatMessageContext.getLanguageModel().getProvider().equals(ModelProvider.Ollama)) { + int inputContextTokens = 0; + if (chatMessageContext.getContext() != null) { + Encoding encodingForProvider = ProjectContentService.getEncodingForProvider(chatMessageContext.getLanguageModel().getProvider()); + inputContextTokens = encodingForProvider.encode(chatMessageContext.getContext()).size(); + } + tokenUsage = new TokenUsage(tokenUsage.inputTokenCount() + inputContextTokens, tokenUsage.outputTokenCount()); + } + return tokenUsage; + } - add(new ResponseHeaderPanel(chatMessageContext), gbc); + /** + * Add document nodes to the panel. + * + * @param document the document + */ + private void addDocumentNodesToPanel(@NotNull Node document) { + JPanel jPanel = createPanel(); - gbc.gridy++; - add(new ResponseContentPanel(chatMessageContext), gbc); + Node node = document.getFirstChild(); - gbc.gridy++; - add(new FileListPanel(chatMessageContext, FileListManager.getInstance().getFiles()), gbc); + while (node != null) { + JPanel panel; + if (node instanceof FencedCodeBlock fencedCodeBlock) { + panel = processBlock(fencedCodeBlock); + } else if (node instanceof IndentedCodeBlock indentedCodeBlock) { + panel = processBlock(indentedCodeBlock); + } else { + panel = processBlock((Block) node); + } - gbc.gridy++; - add(new SemanticSearchReferencesPanel(chatMessageContext, chatMessageContext.getSemanticReferences()), gbc); + setFullWidth(panel); + jPanel.add(panel); + node = node.getNext(); + } + + add(jPanel); + } + + private void setFullWidth(@NotNull JPanel panel) { + Dimension maximumSize = new Dimension(Integer.MAX_VALUE, Integer.MAX_VALUE); + panel.setMaximumSize(maximumSize); + panel.setMinimumSize(new Dimension(panel.getPreferredSize().width, panel.getPreferredSize().height)); + } + + /** + * Create a panel. + * + * @return the panel + */ + private @NotNull JPanel createPanel() { + JPanel jPanel = new JPanel(); + jPanel.setLayout(new BoxLayout(jPanel, BoxLayout.Y_AXIS)); + jPanel.setOpaque(false); + jPanel.setFont(SourceCodeProFontPlan14); + return jPanel; + } - gbc.gridy++; - JPanel metricPanelWrapper = new JPanel(new FlowLayout(FlowLayout.LEFT)); - metricPanelWrapper.add(new MetricExecutionInfoPanel(chatMessageContext)); - add(metricPanelWrapper, gbc); + /** + * Process a block and return a panel. + * + * @param theBlock the block + * @return the panel + */ + private JPanel processBlock(Block theBlock) { + return NodeProcessorFactory.createProcessor(chatMessageContext, theBlock).processNode(); } } diff --git a/src/main/java/com/devoxx/genie/ui/panel/ChatStreamingResponsePanel.java b/src/main/java/com/devoxx/genie/ui/panel/ChatStreamingResponsePanel.java index 35a53611..44445d46 100644 --- a/src/main/java/com/devoxx/genie/ui/panel/ChatStreamingResponsePanel.java +++ b/src/main/java/com/devoxx/genie/ui/panel/ChatStreamingResponsePanel.java @@ -3,7 +3,6 @@ import com.devoxx.genie.model.request.ChatMessageContext; import com.devoxx.genie.service.FileListManager; import com.devoxx.genie.ui.component.ExpandablePanel; -import com.devoxx.genie.ui.panel.chatresponse.ResponseHeaderPanel; import com.devoxx.genie.ui.renderer.CodeBlockNodeRenderer; import com.intellij.openapi.application.ApplicationManager; import com.intellij.openapi.vfs.VirtualFile; diff --git a/src/main/java/com/devoxx/genie/ui/panel/chatresponse/ResponseHeaderPanel.java b/src/main/java/com/devoxx/genie/ui/panel/ResponseHeaderPanel.java similarity index 98% rename from src/main/java/com/devoxx/genie/ui/panel/chatresponse/ResponseHeaderPanel.java rename to src/main/java/com/devoxx/genie/ui/panel/ResponseHeaderPanel.java index fd312b10..866cf5e5 100644 --- a/src/main/java/com/devoxx/genie/ui/panel/chatresponse/ResponseHeaderPanel.java +++ b/src/main/java/com/devoxx/genie/ui/panel/ResponseHeaderPanel.java @@ -1,4 +1,4 @@ -package com.devoxx.genie.ui.panel.chatresponse; +package com.devoxx.genie.ui.panel; import com.devoxx.genie.model.LanguageModel; import com.devoxx.genie.model.request.ChatMessageContext; diff --git a/src/main/java/com/devoxx/genie/ui/panel/chatresponse/FileListPanel.java b/src/main/java/com/devoxx/genie/ui/panel/chatresponse/FileListPanel.java deleted file mode 100644 index ec41cd75..00000000 --- a/src/main/java/com/devoxx/genie/ui/panel/chatresponse/FileListPanel.java +++ /dev/null @@ -1,19 +0,0 @@ -package com.devoxx.genie.ui.panel.chatresponse; - -import com.devoxx.genie.model.request.ChatMessageContext; -import com.devoxx.genie.ui.component.ExpandablePanel; -import com.intellij.openapi.vfs.VirtualFile; - -import javax.swing.*; -import java.util.List; - -// FileListPanel.java -public class FileListPanel extends JPanel { - public FileListPanel(ChatMessageContext chatMessageContext, List files) { - setLayout(new BoxLayout(this, BoxLayout.Y_AXIS)); - setOpaque(false); - if (chatMessageContext.hasFiles()) { - add(new ExpandablePanel(chatMessageContext, files)); - } - } -} diff --git a/src/main/java/com/devoxx/genie/ui/panel/chatresponse/MetricExecutionInfoPanel.java b/src/main/java/com/devoxx/genie/ui/panel/chatresponse/MetricExecutionInfoPanel.java deleted file mode 100644 index 9c79b6fe..00000000 --- a/src/main/java/com/devoxx/genie/ui/panel/chatresponse/MetricExecutionInfoPanel.java +++ /dev/null @@ -1,65 +0,0 @@ -package com.devoxx.genie.ui.panel.chatresponse; - -import com.devoxx.genie.model.request.ChatMessageContext; -import com.devoxx.genie.ui.settings.DevoxxGenieStateService; -import com.devoxx.genie.util.DefaultLLMSettingsUtil; -import com.intellij.ui.JBColor; -import dev.langchain4j.model.output.TokenUsage; - -import javax.swing.*; -import java.awt.*; -import java.text.NumberFormat; -import java.util.Locale; - -// MetricExecutionInfoPanel.java -public class MetricExecutionInfoPanel extends JPanel { - - private static final float METRIC_FONT_SIZE = 12f; - private static final double MS_TO_SECONDS = 1000.0; - - public MetricExecutionInfoPanel(ChatMessageContext chatMessageContext) { - setLayout(new FlowLayout(FlowLayout.LEFT)); - setOpaque(false); - - if (shouldShowExecutionTime(chatMessageContext)) { - add(createMetricLabel(chatMessageContext)); - } - } - - private boolean shouldShowExecutionTime(ChatMessageContext chatMessageContext) { - return Boolean.TRUE.equals(DevoxxGenieStateService.getInstance().getShowExecutionTime()); - } - - private JLabel createMetricLabel(ChatMessageContext chatMessageContext) { - String metricInfo = buildMetricInfo(chatMessageContext); - JLabel label = new JLabel(metricInfo); - label.setForeground(JBColor.GRAY); - label.setFont(label.getFont().deriveFont(METRIC_FONT_SIZE)); - return label; - } - - private String buildMetricInfo(ChatMessageContext chatMessageContext) { - String metricInfoLabel = String.format("ϟ %.2fs", chatMessageContext.getExecutionTimeMs() / MS_TO_SECONDS); - TokenUsage tokenUsage = chatMessageContext.getTokenUsage(); - if (tokenUsage != null) { - metricInfoLabel = buildTokenUsageLabel(tokenUsage, metricInfoLabel, chatMessageContext); - } - return metricInfoLabel; - } - - private String buildTokenUsageLabel(TokenUsage tokenUsage, String metricInfoLabel, ChatMessageContext chatMessageContext) { - String cost = ""; - if (DefaultLLMSettingsUtil.isApiKeyBasedProvider(chatMessageContext.getLanguageModel().getProvider())) { - cost = String.format("- %.5f $", chatMessageContext.getCost()); - } - - // ... (Implementation for calculating token usage, you can move the relevant code here) - - NumberFormat numberFormat = NumberFormat.getNumberInstance(Locale.getDefault()); - String formattedInputTokens = numberFormat.format(tokenUsage.inputTokenCount()); - String formattedOutputTokens = numberFormat.format(tokenUsage.outputTokenCount()); - - metricInfoLabel += String.format(" - Tokens ↑ %s ↓️ %s %s", formattedInputTokens, formattedOutputTokens, cost); - return metricInfoLabel; - } -} diff --git a/src/main/java/com/devoxx/genie/ui/panel/chatresponse/ResponseContentPanel.java b/src/main/java/com/devoxx/genie/ui/panel/chatresponse/ResponseContentPanel.java deleted file mode 100644 index 3db14852..00000000 --- a/src/main/java/com/devoxx/genie/ui/panel/chatresponse/ResponseContentPanel.java +++ /dev/null @@ -1,71 +0,0 @@ -package com.devoxx.genie.ui.panel.chatresponse; - -import com.devoxx.genie.model.request.ChatMessageContext; -import com.devoxx.genie.ui.processor.NodeProcessorFactory; -import com.devoxx.genie.ui.settings.DevoxxGenieStateService; -import org.commonmark.node.Block; -import org.commonmark.node.Node; -import org.commonmark.parser.Parser; - -import javax.swing.*; -import java.awt.*; - -// ResponseContentPanel.java -public class ResponseContentPanel extends JPanel { - - private final transient ChatMessageContext chatMessageContext; - - public ResponseContentPanel(ChatMessageContext chatMessageContext) { - this.chatMessageContext = chatMessageContext; - setLayout(new BoxLayout(this, BoxLayout.Y_AXIS)); - setOpaque(false); - - Node document = parseMarkdown(chatMessageContext.getAiMessage().text()); - processGitDiffIfEnabled(document); - addDocumentNodesToPanel(document); - } - - private Node parseMarkdown(String markdownText) { - return Parser.builder().build().parse(markdownText); - } - - private void processGitDiffIfEnabled(Node document) { - if (isGitDiffEnabled()) { - // ... (Implementation for processing Git diff, you can move the relevant code here) - } - } - - private boolean isGitDiffEnabled() { - return Boolean.TRUE.equals(DevoxxGenieStateService.getInstance().getGitDiffActivated()); - } - - private void addDocumentNodesToPanel(Node document) { - Node node = document.getFirstChild(); - while (node != null) { - addNodeToPanel(node); - node = node.getNext(); - } - } - - private void addNodeToPanel(Node node) { - JPanel nodePanel = processNode(node); - setFullWidth(nodePanel); - add(nodePanel); - } - - private JPanel processNode(Node node) { - if (node instanceof Block block) { - return processBlock(block); - } - return new JPanel(); - } - - private JPanel processBlock(Block block) { - return NodeProcessorFactory.createProcessor(chatMessageContext, block).processNode(); - } - - private void setFullWidth(JPanel panel) { - panel.setMaximumSize(new Dimension(Integer.MAX_VALUE, Integer.MAX_VALUE)); - panel.setMinimumSize(new Dimension(panel.getPreferredSize().width, panel.getPreferredSize().height)); - } -} diff --git a/src/main/java/com/devoxx/genie/ui/panel/chatresponse/SemanticSearchReferencesPanel.java b/src/main/java/com/devoxx/genie/ui/panel/chatresponse/SemanticSearchReferencesPanel.java deleted file mode 100644 index 8e800617..00000000 --- a/src/main/java/com/devoxx/genie/ui/panel/chatresponse/SemanticSearchReferencesPanel.java +++ /dev/null @@ -1,22 +0,0 @@ -package com.devoxx.genie.ui.panel.chatresponse; - -import com.devoxx.genie.model.request.ChatMessageContext; -import com.devoxx.genie.model.request.SemanticFile; -import com.devoxx.genie.ui.component.ExpandablePanel; - -import javax.swing.*; -import java.util.List; - -// SemanticSearchReferencesPanel.java -public class SemanticSearchReferencesPanel extends JPanel { - public SemanticSearchReferencesPanel(ChatMessageContext chatMessageContext, List semanticReferences) { - setLayout(new BoxLayout(this, BoxLayout.Y_AXIS)); - setOpaque(false); - - if (semanticReferences != null && !semanticReferences.isEmpty()) { - ExpandablePanel semanticPanel = new ExpandablePanel(chatMessageContext.getProject(), semanticReferences); - semanticPanel.setName(chatMessageContext.getId() + "_semantic"); - add(semanticPanel); - } - } -} From e03389bbb8bfbca64b75157431adb013ed23b891 Mon Sep 17 00:00:00 2001 From: samir Date: Wed, 18 Dec 2024 12:36:15 +0100 Subject: [PATCH 2/2] fix(issue-243): Fixing UI issue + refactoring of ChatResponsePanel --- .../genie/ui/panel/ChatResponsePanel.java | 219 +----------------- .../ui/panel/ChatStreamingResponsePanel.java | 1 + .../ui/panel/chatresponse/FileListPanel.java | 12 + .../MetricExecutionInfoPanel.java | 58 +++++ .../chatresponse/ResponseDocumentPanel.java | 124 ++++++++++ .../ResponseHeaderPanel.java | 4 +- .../SemanticSearchReferencesPanel.java | 12 + 7 files changed, 216 insertions(+), 214 deletions(-) create mode 100644 src/main/java/com/devoxx/genie/ui/panel/chatresponse/FileListPanel.java create mode 100644 src/main/java/com/devoxx/genie/ui/panel/chatresponse/MetricExecutionInfoPanel.java create mode 100644 src/main/java/com/devoxx/genie/ui/panel/chatresponse/ResponseDocumentPanel.java rename src/main/java/com/devoxx/genie/ui/panel/{ => chatresponse}/ResponseHeaderPanel.java (97%) create mode 100644 src/main/java/com/devoxx/genie/ui/panel/chatresponse/SemanticSearchReferencesPanel.java 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 3f80061c..7a0a0774 100644 --- a/src/main/java/com/devoxx/genie/ui/panel/ChatResponsePanel.java +++ b/src/main/java/com/devoxx/genie/ui/panel/ChatResponsePanel.java @@ -1,235 +1,30 @@ package com.devoxx.genie.ui.panel; -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.model.request.SemanticFile; -import com.devoxx.genie.service.FileListManager; -import com.devoxx.genie.service.ProjectContentService; -import com.devoxx.genie.service.gitdiff.GitMergeService; -import com.devoxx.genie.ui.component.ExpandablePanel; -import com.devoxx.genie.ui.processor.NodeProcessorFactory; +import com.devoxx.genie.ui.panel.chatresponse.*; import com.devoxx.genie.ui.settings.DevoxxGenieStateService; -import com.devoxx.genie.util.DefaultLLMSettingsUtil; -import com.intellij.openapi.vfs.VirtualFile; -import com.intellij.ui.JBColor; -import com.knuddels.jtokkit.api.Encoding; -import dev.langchain4j.model.output.TokenUsage; -import org.commonmark.node.Block; -import org.commonmark.node.FencedCodeBlock; -import org.commonmark.node.IndentedCodeBlock; -import org.commonmark.node.Node; -import org.commonmark.parser.Parser; import org.jetbrains.annotations.NotNull; - import javax.swing.*; -import java.awt.*; -import java.text.NumberFormat; -import java.util.ArrayList; -import java.util.List; -import java.util.Locale; - -import static com.devoxx.genie.ui.util.DevoxxGenieFontsUtil.SourceCodeProFontPlan14; public class ChatResponsePanel extends BackgroundPanel { - private final transient ChatMessageContext chatMessageContext; - - /** - * Create a new chat response panel. - * - * @param chatMessageContext the chat message context - */ public ChatResponsePanel(@NotNull ChatMessageContext chatMessageContext) { super(chatMessageContext.getId()); setLayout(new BoxLayout(this, BoxLayout.Y_AXIS)); - this.chatMessageContext = chatMessageContext; - add(new ResponseHeaderPanel(chatMessageContext)); - addResponsePane(chatMessageContext); - } - - /** - * Get the response pane with rendered HTML. - * - * @param chatMessageContext the chat message context - */ - private void addResponsePane(@NotNull ChatMessageContext chatMessageContext) { - String markDownResponse = chatMessageContext.getAiMessage().text(); - Node document = Parser.builder().build().parse(markDownResponse); - - DevoxxGenieStateService stateService = DevoxxGenieStateService.getInstance(); + add(new ResponseDocumentPanel(chatMessageContext)); - // If git diff is activated, try to extract code blocks and show diff - if (Boolean.TRUE.equals(stateService.getGitDiffActivated())) { - processGitDiff(chatMessageContext, document); - } - - addDocumentNodesToPanel(document); - - // Add regular files panel if (chatMessageContext.hasFiles()) { - java.util.List files = FileListManager.getInstance().getFiles(); - ExpandablePanel fileListPanel = new ExpandablePanel(chatMessageContext, files); - add(fileListPanel); + add(new FileListPanel(chatMessageContext)); } - // Add semantic references panel - addSemanticSearchReferences(chatMessageContext); - - if (Boolean.TRUE.equals(DevoxxGenieStateService.getInstance().getShowExecutionTime())) { - // Add execution time, token usage and cost information - addMetricExecutionInfo(chatMessageContext); + if (chatMessageContext.getSemanticReferences() != null && !chatMessageContext.getSemanticReferences().isEmpty()) { + add(new SemanticSearchReferencesPanel(chatMessageContext)); } - } - - private void addSemanticSearchReferences(@NotNull ChatMessageContext chatMessageContext) { - List semanticReferences = chatMessageContext.getSemanticReferences(); - if (semanticReferences != null && !semanticReferences.isEmpty()) { - ExpandablePanel semanticPanel = new ExpandablePanel(chatMessageContext.getProject(), semanticReferences); - semanticPanel.setName(chatMessageContext.getId() + "_semantic"); - add(semanticPanel); - } - } - - private void addMetricExecutionInfo(@NotNull ChatMessageContext chatMessageContext) { - JPanel metricExecutionInfoPanel = new JPanel(new FlowLayout(FlowLayout.LEFT)); - metricExecutionInfoPanel.setOpaque(false); - - String metricInfoLabel = String.format("ϟ %.2fs", chatMessageContext.getExecutionTimeMs() / 1000.0); - - dev.langchain4j.model.output.TokenUsage tokenUsage = chatMessageContext.getTokenUsage(); - if (tokenUsage != null) { - - String cost = ""; - if (DefaultLLMSettingsUtil.isApiKeyBasedProvider(chatMessageContext.getLanguageModel().getProvider())) { - cost = String.format("- %.5f $", chatMessageContext.getCost()); - } - - tokenUsage = calcOllamaInputTokenCount(chatMessageContext, tokenUsage); - - NumberFormat numberFormat = NumberFormat.getNumberInstance(Locale.getDefault()); - String formattedInputTokens = numberFormat.format(tokenUsage.inputTokenCount()); - String formattedOutputTokens = numberFormat.format(tokenUsage.outputTokenCount()); - - metricInfoLabel += String.format(" - Tokens ↑ %s ↓️ %s %s", formattedInputTokens, formattedOutputTokens, cost); - } - - JLabel tokenLabel = new JLabel(metricInfoLabel); - tokenLabel.setForeground(JBColor.GRAY); - tokenLabel.setFont(tokenLabel.getFont().deriveFont(12f)); - - metricExecutionInfoPanel.add(tokenLabel); - add(metricExecutionInfoPanel); - } - - /** - * Process the git diff. - * @param chatMessageContext the chat message context - * @param document the document - */ - private void processGitDiff(@NotNull ChatMessageContext chatMessageContext, @NotNull Node document) { - // Get original file info - EditorInfo editorInfo = chatMessageContext.getEditorInfo(); - if (editorInfo == null) { - return; - } - - if (editorInfo.getSelectedFiles() != null && !editorInfo.getSelectedFiles().isEmpty()) { - List files = editorInfo.getSelectedFiles(); - List modifiedContents = new ArrayList<>(); - - // Collect modified contents from code blocks - Node node = document.getFirstChild(); - while (node != null) { - if (node instanceof FencedCodeBlock codeBlock) { - modifiedContents.add(codeBlock.getLiteral()); - } - node = node.getNext(); - } - - GitMergeService.getInstance().showDiffView( - chatMessageContext.getProject(), - files.get(0), - modifiedContents.get(0)); - } - } - - /** - * Ollama does not count the input context tokens in the token usage, this method fixes this. - * - * @param chatMessageContext the chat message context - * @param tokenUsage the token usage - * @return the updated token usage - */ - private static TokenUsage calcOllamaInputTokenCount(@NotNull ChatMessageContext chatMessageContext, TokenUsage tokenUsage) { - if (chatMessageContext.getLanguageModel().getProvider().equals(ModelProvider.Ollama)) { - int inputContextTokens = 0; - if (chatMessageContext.getContext() != null) { - Encoding encodingForProvider = ProjectContentService.getEncodingForProvider(chatMessageContext.getLanguageModel().getProvider()); - inputContextTokens = encodingForProvider.encode(chatMessageContext.getContext()).size(); - } - tokenUsage = new TokenUsage(tokenUsage.inputTokenCount() + inputContextTokens, tokenUsage.outputTokenCount()); - } - return tokenUsage; - } - - /** - * Add document nodes to the panel. - * - * @param document the document - */ - private void addDocumentNodesToPanel(@NotNull Node document) { - JPanel jPanel = createPanel(); - - Node node = document.getFirstChild(); - - while (node != null) { - JPanel panel; - if (node instanceof FencedCodeBlock fencedCodeBlock) { - panel = processBlock(fencedCodeBlock); - } else if (node instanceof IndentedCodeBlock indentedCodeBlock) { - panel = processBlock(indentedCodeBlock); - } else { - panel = processBlock((Block) node); - } - - setFullWidth(panel); - jPanel.add(panel); - node = node.getNext(); + if (Boolean.TRUE.equals(DevoxxGenieStateService.getInstance().getShowExecutionTime())) { + add(new MetricExecutionInfoPanel(chatMessageContext)); } - - add(jPanel); - } - - private void setFullWidth(@NotNull JPanel panel) { - Dimension maximumSize = new Dimension(Integer.MAX_VALUE, Integer.MAX_VALUE); - panel.setMaximumSize(maximumSize); - panel.setMinimumSize(new Dimension(panel.getPreferredSize().width, panel.getPreferredSize().height)); - } - - /** - * Create a panel. - * - * @return the panel - */ - private @NotNull JPanel createPanel() { - JPanel jPanel = new JPanel(); - jPanel.setLayout(new BoxLayout(jPanel, BoxLayout.Y_AXIS)); - jPanel.setOpaque(false); - jPanel.setFont(SourceCodeProFontPlan14); - return jPanel; - } - - /** - * Process a block and return a panel. - * - * @param theBlock the block - * @return the panel - */ - private JPanel processBlock(Block theBlock) { - return NodeProcessorFactory.createProcessor(chatMessageContext, theBlock).processNode(); } } diff --git a/src/main/java/com/devoxx/genie/ui/panel/ChatStreamingResponsePanel.java b/src/main/java/com/devoxx/genie/ui/panel/ChatStreamingResponsePanel.java index 44445d46..35a53611 100644 --- a/src/main/java/com/devoxx/genie/ui/panel/ChatStreamingResponsePanel.java +++ b/src/main/java/com/devoxx/genie/ui/panel/ChatStreamingResponsePanel.java @@ -3,6 +3,7 @@ import com.devoxx.genie.model.request.ChatMessageContext; import com.devoxx.genie.service.FileListManager; import com.devoxx.genie.ui.component.ExpandablePanel; +import com.devoxx.genie.ui.panel.chatresponse.ResponseHeaderPanel; import com.devoxx.genie.ui.renderer.CodeBlockNodeRenderer; import com.intellij.openapi.application.ApplicationManager; import com.intellij.openapi.vfs.VirtualFile; diff --git a/src/main/java/com/devoxx/genie/ui/panel/chatresponse/FileListPanel.java b/src/main/java/com/devoxx/genie/ui/panel/chatresponse/FileListPanel.java new file mode 100644 index 00000000..f5383660 --- /dev/null +++ b/src/main/java/com/devoxx/genie/ui/panel/chatresponse/FileListPanel.java @@ -0,0 +1,12 @@ +package com.devoxx.genie.ui.panel.chatresponse; + +import com.devoxx.genie.model.request.ChatMessageContext; +import com.devoxx.genie.service.FileListManager; +import com.devoxx.genie.ui.component.ExpandablePanel; + +public class FileListPanel extends ExpandablePanel { + + public FileListPanel(ChatMessageContext chatMessageContext) { + super(chatMessageContext, FileListManager.getInstance().getFiles()); + } +} diff --git a/src/main/java/com/devoxx/genie/ui/panel/chatresponse/MetricExecutionInfoPanel.java b/src/main/java/com/devoxx/genie/ui/panel/chatresponse/MetricExecutionInfoPanel.java new file mode 100644 index 00000000..3af52625 --- /dev/null +++ b/src/main/java/com/devoxx/genie/ui/panel/chatresponse/MetricExecutionInfoPanel.java @@ -0,0 +1,58 @@ +package com.devoxx.genie.ui.panel.chatresponse; + +import com.devoxx.genie.model.enumarations.ModelProvider; +import com.devoxx.genie.model.request.ChatMessageContext; +import com.devoxx.genie.service.ProjectContentService; +import com.devoxx.genie.util.DefaultLLMSettingsUtil; +import com.intellij.ui.JBColor; +import com.knuddels.jtokkit.api.Encoding; +import dev.langchain4j.model.output.TokenUsage; + +import javax.swing.*; +import java.awt.*; +import java.text.NumberFormat; +import java.util.Locale; + +public class MetricExecutionInfoPanel extends JPanel { + + public MetricExecutionInfoPanel(ChatMessageContext chatMessageContext) { + setLayout(new FlowLayout(FlowLayout.LEFT)); + setOpaque(false); + + String metricInfoLabel = String.format("ϟ %.2fs", chatMessageContext.getExecutionTimeMs() / 1000.0); + TokenUsage tokenUsage = chatMessageContext.getTokenUsage(); + + if (tokenUsage != null) { + String cost = ""; + if (DefaultLLMSettingsUtil.isApiKeyBasedProvider(chatMessageContext.getLanguageModel().getProvider())) { + cost = String.format("- %.5f $", chatMessageContext.getCost()); + } + + tokenUsage = calcOllamaInputTokenCount(chatMessageContext, tokenUsage); + + NumberFormat numberFormat = NumberFormat.getNumberInstance(Locale.getDefault()); + String formattedInputTokens = numberFormat.format(tokenUsage.inputTokenCount()); + String formattedOutputTokens = numberFormat.format(tokenUsage.outputTokenCount()); + + metricInfoLabel += String.format(" - Tokens ↑ %s ↓️ %s %s", formattedInputTokens, formattedOutputTokens, cost); + } + + + JLabel tokenLabel = new JLabel(metricInfoLabel); + tokenLabel.setForeground(JBColor.GRAY); + tokenLabel.setFont(tokenLabel.getFont().deriveFont(12f)); + add(tokenLabel); + } + + private static TokenUsage calcOllamaInputTokenCount(ChatMessageContext chatMessageContext, TokenUsage tokenUsage) { + if (chatMessageContext.getLanguageModel().getProvider().equals(ModelProvider.Ollama)) { + int inputContextTokens = 0; + if (chatMessageContext.getContext() != null) { + Encoding encodingForProvider = ProjectContentService.getEncodingForProvider(chatMessageContext.getLanguageModel().getProvider()); + inputContextTokens = encodingForProvider.encode(chatMessageContext.getContext()).size(); + } + tokenUsage = new TokenUsage(tokenUsage.inputTokenCount() + inputContextTokens, tokenUsage.outputTokenCount()); + } + return tokenUsage; + } +} diff --git a/src/main/java/com/devoxx/genie/ui/panel/chatresponse/ResponseDocumentPanel.java b/src/main/java/com/devoxx/genie/ui/panel/chatresponse/ResponseDocumentPanel.java new file mode 100644 index 00000000..aa379ef5 --- /dev/null +++ b/src/main/java/com/devoxx/genie/ui/panel/chatresponse/ResponseDocumentPanel.java @@ -0,0 +1,124 @@ +package com.devoxx.genie.ui.panel.chatresponse; + +import com.devoxx.genie.model.request.ChatMessageContext; +import com.devoxx.genie.model.request.EditorInfo; +import com.devoxx.genie.service.gitdiff.GitMergeService; +import com.devoxx.genie.ui.processor.NodeProcessorFactory; +import com.devoxx.genie.ui.settings.DevoxxGenieStateService; +import com.intellij.openapi.vfs.VirtualFile; +import org.commonmark.node.Block; +import org.commonmark.node.FencedCodeBlock; +import org.commonmark.node.IndentedCodeBlock; +import org.commonmark.node.Node; +import org.commonmark.parser.Parser; +import org.jetbrains.annotations.NotNull; + +import javax.swing.*; +import java.awt.*; +import java.util.ArrayList; +import java.util.List; + +public class ResponseDocumentPanel extends JPanel { + + + private final transient ChatMessageContext chatMessageContext; + + public ResponseDocumentPanel(@NotNull ChatMessageContext chatMessageContext) { + setLayout(new BoxLayout(this, BoxLayout.Y_AXIS)); + + this.chatMessageContext = chatMessageContext; + + String markDownResponse = chatMessageContext.getAiMessage().text(); + + System.out.println(markDownResponse); + + Node document = Parser.builder().build().parse(markDownResponse); + + DevoxxGenieStateService stateService = DevoxxGenieStateService.getInstance(); + + if (Boolean.TRUE.equals(stateService.getGitDiffActivated())) { + processGitDiff(chatMessageContext, document); + } + + addDocumentNodesToPanel(document); + } + + private void processGitDiff(@NotNull ChatMessageContext chatMessageContext, @NotNull Node document) { + // Get original file info + EditorInfo editorInfo = chatMessageContext.getEditorInfo(); + if (editorInfo == null) { + return; + } + + if (editorInfo.getSelectedFiles() != null && !editorInfo.getSelectedFiles().isEmpty()) { + List files = editorInfo.getSelectedFiles(); + List modifiedContents = new ArrayList<>(); + + // Collect modified contents from code blocks + Node node = document.getFirstChild(); + while (node != null) { + if (node instanceof FencedCodeBlock codeBlock) { + modifiedContents.add(codeBlock.getLiteral()); + } + node = node.getNext(); + } + + GitMergeService.getInstance().showDiffView( + chatMessageContext.getProject(), + files.get(0), + modifiedContents.isEmpty() ? "" : modifiedContents.get(0)); + } + } + + + /** + * Add document nodes to the panel. + * + * @param document the document + */ + private void addDocumentNodesToPanel(@NotNull Node document) { + JPanel jPanel = createPanel(); + + Node node = document.getFirstChild(); + + while (node != null) { + JPanel panel; + if (node instanceof FencedCodeBlock fencedCodeBlock) { + panel = processBlock(fencedCodeBlock); + } else if (node instanceof IndentedCodeBlock indentedCodeBlock) { + panel = processBlock(indentedCodeBlock); + } else { + panel = processBlock((Block) node); + } + + setFullWidth(panel); + jPanel.add(panel); + node = node.getNext(); + } + + add(jPanel); + } + + private void setFullWidth(@NotNull JPanel panel) { + Dimension maximumSize = new Dimension(Integer.MAX_VALUE, Integer.MAX_VALUE); + panel.setMaximumSize(maximumSize); + panel.setMinimumSize(new Dimension(panel.getPreferredSize().width, panel.getPreferredSize().height)); + } + + private @NotNull JPanel createPanel() { + JPanel jPanel = new JPanel(); + jPanel.setLayout(new BoxLayout(jPanel, BoxLayout.Y_AXIS)); + jPanel.setOpaque(false); + return jPanel; + } + + /** + * Process a block and return a panel. + * + * @param theBlock the block + * @return the panel + */ + private JPanel processBlock(Block theBlock) { + return NodeProcessorFactory.createProcessor(chatMessageContext, theBlock).processNode(); + } +} diff --git a/src/main/java/com/devoxx/genie/ui/panel/ResponseHeaderPanel.java b/src/main/java/com/devoxx/genie/ui/panel/chatresponse/ResponseHeaderPanel.java similarity index 97% rename from src/main/java/com/devoxx/genie/ui/panel/ResponseHeaderPanel.java rename to src/main/java/com/devoxx/genie/ui/panel/chatresponse/ResponseHeaderPanel.java index 866cf5e5..633f3bf6 100644 --- a/src/main/java/com/devoxx/genie/ui/panel/ResponseHeaderPanel.java +++ b/src/main/java/com/devoxx/genie/ui/panel/chatresponse/ResponseHeaderPanel.java @@ -1,4 +1,4 @@ -package com.devoxx.genie.ui.panel; +package com.devoxx.genie.ui.panel.chatresponse; import com.devoxx.genie.model.LanguageModel; import com.devoxx.genie.model.request.ChatMessageContext; @@ -26,7 +26,7 @@ public class ResponseHeaderPanel extends JBPanel { */ public ResponseHeaderPanel(@NotNull ChatMessageContext chatMessageContext) { super(new BorderLayout()); - + setBackground(Color.BLUE); andTransparent() .withMaximumHeight(30) .withPreferredHeight(30); diff --git a/src/main/java/com/devoxx/genie/ui/panel/chatresponse/SemanticSearchReferencesPanel.java b/src/main/java/com/devoxx/genie/ui/panel/chatresponse/SemanticSearchReferencesPanel.java new file mode 100644 index 00000000..9ab63f31 --- /dev/null +++ b/src/main/java/com/devoxx/genie/ui/panel/chatresponse/SemanticSearchReferencesPanel.java @@ -0,0 +1,12 @@ +package com.devoxx.genie.ui.panel.chatresponse; + +import com.devoxx.genie.model.request.ChatMessageContext; +import com.devoxx.genie.ui.component.ExpandablePanel; + +public class SemanticSearchReferencesPanel extends ExpandablePanel { + + public SemanticSearchReferencesPanel(ChatMessageContext chatMessageContext) { + super(chatMessageContext.getProject(), chatMessageContext.getSemanticReferences()); + setName(chatMessageContext.getId() + "_semantic"); + } +}