InferredRuleDetailPanel.java
/*******************************************************************************
* Copyright (c) 2026 Carsten Hammer.
*
* This program and the accompanying materials
* are made available under the terms of the Eclipse Public License 2.0
* which accompanies this distribution, and is available at
* https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*
* Contributors:
* Carsten Hammer
*******************************************************************************/
package org.sandbox.jdt.internal.ui.views.mining;
import java.util.ArrayList;
import java.util.List;
import org.eclipse.swt.SWT;
import org.eclipse.swt.custom.ScrolledComposite;
import org.eclipse.swt.custom.StyledText;
import org.eclipse.swt.layout.GridData;
import org.eclipse.swt.layout.GridLayout;
import org.eclipse.swt.widgets.Button;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Label;
import org.sandbox.jdt.triggerpattern.llm.CommitEvaluation;
import org.sandbox.jdt.triggerpattern.mining.analysis.InferredRule;
/**
* Detail panel showing inferred rules or AI evaluations for a selected commit.
*
* <p>Displays each rule/evaluation with its source pattern, confidence,
* traffic light assessment, and a checkbox for selection. Action buttons
* allow adopting rules into the HintFileRegistry or exporting as
* {@code .sandbox-hint}.</p>
*
* @since 1.2.6
*/
public class InferredRuleDetailPanel extends Composite {
private final ScrolledComposite scrolled;
private final Composite content;
private final Label headerLabel;
private final List<Button> ruleCheckboxes = new ArrayList<>();
private CommitTableEntry currentEntry;
/**
* Creates the detail panel.
*
* @param parent the parent composite
*/
public InferredRuleDetailPanel(Composite parent) {
super(parent, SWT.NONE);
setLayout(new GridLayout(1, false));
headerLabel = new Label(this, SWT.NONE);
headerLabel.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false));
headerLabel.setText("Select a commit with inferred rules (\u2705) to see details."); //$NON-NLS-1$
scrolled = new ScrolledComposite(this, SWT.V_SCROLL | SWT.H_SCROLL);
scrolled.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true));
scrolled.setExpandHorizontal(true);
scrolled.setExpandVertical(true);
content = new Composite(scrolled, SWT.NONE);
content.setLayout(new GridLayout(1, false));
scrolled.setContent(content);
}
/**
* Shows the inferred rules or AI evaluations for the given commit table entry.
*
* @param entry the selected commit entry (may be {@code null} to clear)
*/
public void showRules(CommitTableEntry entry) {
this.currentEntry = entry;
ruleCheckboxes.clear();
// Dispose old content children
for (var child : content.getChildren()) {
child.dispose();
}
if (entry == null || !entry.hasRules()) {
headerLabel.setText("No rules to display."); //$NON-NLS-1$
content.layout();
scrolled.setMinSize(content.computeSize(SWT.DEFAULT, SWT.DEFAULT));
return;
}
headerLabel.setText("Commit " + entry.getCommitInfo().shortId() + ": \"" //$NON-NLS-1$ //$NON-NLS-2$
+ firstLine(entry.getCommitInfo().message()) + "\" \u2014 " //$NON-NLS-1$
+ entry.getRuleCount() + " rule(s)"); //$NON-NLS-1$
List<CommitEvaluation> evals = entry.getEvaluations();
if (!evals.isEmpty()) {
int index = 1;
for (CommitEvaluation eval : evals) {
if (eval.dslRule() != null && !eval.dslRule().isBlank()) {
createEvaluationWidget(content, eval, index++);
}
}
} else {
List<InferredRule> rules = entry.getInferredRules();
for (int i = 0; i < rules.size(); i++) {
createRuleWidget(content, rules.get(i), i + 1);
}
}
content.layout();
scrolled.setMinSize(content.computeSize(SWT.DEFAULT, SWT.DEFAULT));
}
/**
* Returns the DSL rules from selected (checked) evaluations.
*
* @return list of selected DSL rule strings
*/
public List<String> getSelectedDslRules() {
List<String> selected = new ArrayList<>();
if (currentEntry == null || !currentEntry.hasRules()) {
return selected;
}
List<CommitEvaluation> evals = currentEntry.getEvaluations();
if (!evals.isEmpty()) {
int checkIndex = 0;
for (CommitEvaluation eval : evals) {
if (eval.dslRule() != null && !eval.dslRule().isBlank()) {
if (checkIndex < ruleCheckboxes.size()
&& ruleCheckboxes.get(checkIndex).getSelection()) {
selected.add(eval.dslRule());
}
checkIndex++;
}
}
}
return selected;
}
/**
* Returns the currently selected (checked) rules.
*
* @return list of selected inferred rules
*/
public List<InferredRule> getSelectedRules() {
List<InferredRule> selected = new ArrayList<>();
if (currentEntry == null || !currentEntry.hasRules()) {
return selected;
}
List<InferredRule> rules = currentEntry.getInferredRules();
for (int i = 0; i < ruleCheckboxes.size() && i < rules.size(); i++) {
if (ruleCheckboxes.get(i).getSelection()) {
selected.add(rules.get(i));
}
}
return selected;
}
private void createEvaluationWidget(Composite parent, CommitEvaluation eval, int index) {
Composite ruleComposite = new Composite(parent, SWT.BORDER);
ruleComposite.setLayout(new GridLayout(2, false));
ruleComposite.setLayoutData(new GridData(SWT.FILL, SWT.TOP, true, false));
// Checkbox + traffic light header
Button checkbox = new Button(ruleComposite, SWT.CHECK);
checkbox.setSelection(true);
String trafficIcon = switch (eval.trafficLight()) {
case GREEN -> "\uD83D\uDFE2"; //$NON-NLS-1$
case YELLOW -> "\uD83D\uDFE1"; //$NON-NLS-1$
case RED -> "\uD83D\uDD34"; //$NON-NLS-1$
case NOT_APPLICABLE -> "\u2B55"; //$NON-NLS-1$
};
checkbox.setText("Rule " + index + " " + trafficIcon + " " //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
+ (eval.category() != null ? eval.category() : "")); //$NON-NLS-1$
checkbox.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false, 2, 1));
ruleCheckboxes.add(checkbox);
// Summary
if (eval.summary() != null && !eval.summary().isBlank()) {
Label summaryLabel = new Label(ruleComposite, SWT.WRAP);
summaryLabel.setText(eval.summary());
summaryLabel.setLayoutData(new GridData(SWT.FILL, SWT.TOP, true, false, 2, 1));
}
// DSL rule display
StyledText ruleText = new StyledText(ruleComposite, SWT.READ_ONLY | SWT.MULTI | SWT.WRAP | SWT.BORDER);
ruleText.setLayoutData(new GridData(SWT.FILL, SWT.TOP, true, false, 2, 1));
ruleText.setText(eval.dslRule());
ruleText.setEditable(false);
// Validation status
if (eval.dslValidationResult() != null) {
Label validationLabel = new Label(ruleComposite, SWT.NONE);
String validationText = "VALID".equals(eval.dslValidationResult()) //$NON-NLS-1$
? "\u2705 Valid DSL" //$NON-NLS-1$
: "\u26A0\uFE0F " + eval.dslValidationResult(); //$NON-NLS-1$
validationLabel.setText(validationText);
validationLabel.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false, 2, 1));
}
}
private void createRuleWidget(Composite parent, InferredRule rule, int index) {
Composite ruleComposite = new Composite(parent, SWT.BORDER);
ruleComposite.setLayout(new GridLayout(2, false));
ruleComposite.setLayoutData(new GridData(SWT.FILL, SWT.TOP, true, false));
// Checkbox + rule header
Button checkbox = new Button(ruleComposite, SWT.CHECK);
checkbox.setSelection(true);
int confidencePercent = (int) (rule.confidence() * 100);
checkbox.setText("Rule " + index + ": Confidence: " + confidencePercent + "%"); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
checkbox.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false, 2, 1));
ruleCheckboxes.add(checkbox);
// Pattern display
StyledText patternText = new StyledText(ruleComposite, SWT.READ_ONLY | SWT.MULTI | SWT.WRAP);
patternText.setLayoutData(new GridData(SWT.FILL, SWT.TOP, true, false, 2, 1));
StringBuilder sb = new StringBuilder();
sb.append(rule.sourcePattern()).append('\n');
sb.append("=> ").append(rule.replacementPattern()); //$NON-NLS-1$
patternText.setText(sb.toString());
patternText.setEditable(false);
}
private static String firstLine(String message) {
if (message == null) {
return ""; //$NON-NLS-1$
}
int nl = message.indexOf('\n');
return nl > 0 ? message.substring(0, nl) : message;
}
}