Skip to content

Commit

Permalink
Merge pull request #396 from samkerr4coding/feat/issue-243
Browse files Browse the repository at this point in the history
Issue 243 : [REFACTORING] Make ChatResponsePanel more modular
  • Loading branch information
stephanj authored Dec 17, 2024
2 parents e798e41 + 7dd34a5 commit 120e72b
Show file tree
Hide file tree
Showing 8 changed files with 200 additions and 253 deletions.
231 changes: 21 additions & 210 deletions src/main/java/com/devoxx/genie/ui/panel/ChatResponsePanel.java
Original file line number Diff line number Diff line change
@@ -1,235 +1,46 @@
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.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 com.devoxx.genie.ui.panel.chatresponse.*;
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();

// 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<VirtualFile> 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<SemanticFile> 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<VirtualFile> files = editorInfo.getSelectedFiles();
List<String> 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));
}
setLayout(new GridBagLayout());
buildResponsePanel();
}

/**
* 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;
}
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)

/**
* Add document nodes to the panel.
*
* @param document the document
*/
private void addDocumentNodesToPanel(@NotNull Node document) {
JPanel jPanel = createPanel();
add(new ResponseHeaderPanel(chatMessageContext), gbc);

Node node = document.getFirstChild();
gbc.gridy++;
add(new ResponseContentPanel(chatMessageContext), 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 FileListPanel(chatMessageContext, FileListManager.getInstance().getFiles()), 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++;
add(new SemanticSearchReferencesPanel(chatMessageContext, chatMessageContext.getSemanticReferences()), 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();
gbc.gridy++;
JPanel metricPanelWrapper = new JPanel(new FlowLayout(FlowLayout.LEFT));
metricPanelWrapper.add(new MetricExecutionInfoPanel(chatMessageContext));
add(metricPanelWrapper, gbc);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
42 changes: 0 additions & 42 deletions src/main/java/com/devoxx/genie/ui/panel/WarningPanel.java

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
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<VirtualFile> files) {
setLayout(new BoxLayout(this, BoxLayout.Y_AXIS));
setOpaque(false);
if (chatMessageContext.hasFiles()) {
add(new ExpandablePanel(chatMessageContext, files));
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
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;
}
}
Loading

0 comments on commit 120e72b

Please sign in to comment.