Skip to content

Commit e63a95f

Browse files
authored
Merge branch 'main' into jackson3
2 parents bf99f76 + 30b581c commit e63a95f

36 files changed

+202
-25
lines changed

.jbang/JabLsLauncher.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@
4949
//DEPS org.tinylog:slf4j-tinylog:2.7.0
5050
//DEPS org.tinylog:tinylog-impl:2.7.0
5151
//DEPS com.github.ben-manes.caffeine:caffeine:3.2.3
52-
//DEPS org.apache.commons:commons-lang3:3.19.0
52+
//DEPS org.apache.commons:commons-lang3:3.20.0
5353

5454
/// This class is required for [jbang](https://www.jbang.dev/)
5555
public class JabLsLauncher {

.jbang/JabSrvLauncher.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -79,11 +79,11 @@
7979
//DEPS org.jabref:afterburner.fx:2.0.0
8080
//DEPS net.harawata:appdirs:1.5.0
8181
//DEPS de.undercouch:citeproc-java:3.4.0
82-
//DEPS com.github.ben-manes.caffeine:caffeine:3.2.3
82+
//DEPS com.github.ben-manes.caffeine:caffeine:3.2.3
8383
//DEPS tools.jackson.core:jackson-core:3.0.2
8484
//DEPS tools.jackson.core:jackson-databind:3.0.2
8585
//DEPS tools.jackson.dataformat:jackson-dataformat-yaml:3.0.2
86-
//DEPS org.apache.commons:commons-lang3:3.19.0
86+
//DEPS org.apache.commons:commons-lang3:3.20.0
8787

8888
/// This class is required for [jbang](https://www.jbang.dev/)
8989
public class JabSrvLauncher {

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ Note that this project **does not** adhere to [Semantic Versioning](https://semv
1414
- We added "IEEE" as another option for parsing plain text citations. [#14233](github.com/JabRef/jabref/pull/14233)
1515
- We added automatic date-based groups that create year/month/day subgroups from an entry’s date fields. [#10822](https://github.com/JabRef/jabref/issues/10822)
1616
- We added `doi-to-bibtex` to `JabKit`. [#14244](https://github.com/JabRef/jabref/pull/14244)
17+
- We added a "Regenerate" button for the AI chat allowing the user to make the language model reformulate its response to the previous prompt. [#12191](https://github.com/JabRef/jabref/issues/12191)
1718
- We added support for transliteration of fields to English and automatic transliteration of generated citation key. [#11377](https://github.com/JabRef/jabref/issues/11377)
1819

1920
### Changed

build.gradle.kts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ plugins {
33
id("org.jabref.gradle.feature.compile") // for openrewrite
44
id("org.openrewrite.rewrite") version "7.20.0"
55
id("org.itsallcode.openfasttrace") version "3.1.0"
6-
id("org.cyclonedx.bom") version "3.0.1"
6+
id("org.cyclonedx.bom") version "3.0.2"
77
}
88

99
// OpenRewrite should rewrite all sources

jabgui/src/main/java/org/jabref/gui/ai/components/aichat/AiChatComponent.java

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import java.nio.file.Path;
44
import java.util.ArrayList;
55
import java.util.List;
6+
import java.util.Optional;
67
import java.util.stream.Stream;
78

89
import javafx.beans.Observable;
@@ -43,6 +44,7 @@
4344
import com.google.common.annotations.VisibleForTesting;
4445
import dev.langchain4j.data.message.AiMessage;
4546
import dev.langchain4j.data.message.ChatMessage;
47+
import dev.langchain4j.data.message.ChatMessageType;
4648
import dev.langchain4j.data.message.UserMessage;
4749
import org.controlsfx.control.PopOver;
4850
import org.slf4j.Logger;
@@ -191,6 +193,22 @@ private void initializeChatPrompt() {
191193
onSendMessage(userMessage);
192194
});
193195

196+
chatPrompt.setRegenerateCallback(() -> {
197+
setLoading(true);
198+
Optional<UserMessage> lastUserPrompt = Optional.empty();
199+
if (!aiChatLogic.getChatHistory().isEmpty()) {
200+
lastUserPrompt = getLastUserMessage();
201+
}
202+
if (lastUserPrompt.isPresent()) {
203+
while (aiChatLogic.getChatHistory().getLast().type() != ChatMessageType.USER) {
204+
deleteLastMessage();
205+
}
206+
deleteLastMessage();
207+
chatPrompt.switchToNormalState();
208+
onSendMessage(lastUserPrompt.get().singleText());
209+
}
210+
});
211+
194212
chatPrompt.requestPromptFocus();
195213

196214
updatePromptHistory();
@@ -334,4 +352,16 @@ private void deleteLastMessage() {
334352
aiChatLogic.getChatHistory().remove(index);
335353
}
336354
}
355+
356+
private Optional<UserMessage> getLastUserMessage() {
357+
int messageIndex = aiChatLogic.getChatHistory().size() - 1;
358+
while (messageIndex >= 0) {
359+
ChatMessage chat = aiChatLogic.getChatHistory().get(messageIndex);
360+
if (chat.type() == ChatMessageType.USER) {
361+
return Optional.of((UserMessage) chat);
362+
}
363+
messageIndex--;
364+
}
365+
return Optional.empty();
366+
}
337367
}

jabgui/src/main/java/org/jabref/gui/ai/components/aichat/chatprompt/ChatPromptComponent.java

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ public class ChatPromptComponent extends HBox {
3030
private final ObjectProperty<Consumer<String>> sendCallback = new SimpleObjectProperty<>();
3131
private final ObjectProperty<Consumer<String>> retryCallback = new SimpleObjectProperty<>();
3232
private final ObjectProperty<Runnable> cancelCallback = new SimpleObjectProperty<>();
33+
private final ObjectProperty<Runnable> regenerateCallback = new SimpleObjectProperty<>();
3334

3435
private final ListProperty<String> history = new SimpleListProperty<>(FXCollections.observableArrayList());
3536

@@ -44,6 +45,7 @@ public class ChatPromptComponent extends HBox {
4445

4546
@FXML private ExpandingTextArea userPromptTextArea;
4647
@FXML private Button submitButton;
48+
@FXML private Button regenerateButton;
4749

4850
public ChatPromptComponent() {
4951
ViewLoader.view(this)
@@ -68,6 +70,10 @@ public void setCancelCallback(Runnable cancelCallback) {
6870
this.cancelCallback.set(cancelCallback);
6971
}
7072

73+
public void setRegenerateCallback(Runnable regenerateCallback) {
74+
this.regenerateCallback.set(regenerateCallback);
75+
}
76+
7177
public ListProperty<String> getHistory() {
7278
return history;
7379
}
@@ -174,6 +180,7 @@ public void switchToNormalState() {
174180
this.getChildren().clear();
175181
this.getChildren().add(userPromptTextArea);
176182
this.getChildren().add(submitButton);
183+
this.getChildren().add(regenerateButton);
177184
requestPromptFocus();
178185
}
179186

@@ -191,4 +198,11 @@ private void onSendMessage() {
191198
sendCallback.get().accept(userPrompt);
192199
}
193200
}
201+
202+
@FXML
203+
private void onRegenerateMessage() {
204+
if (regenerateCallback.get() != null) {
205+
regenerateCallback.get().run();
206+
}
207+
}
194208
}

jabgui/src/main/java/org/jabref/gui/groups/GroupNodeViewModel.java

Lines changed: 49 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -272,7 +272,7 @@ private void onDatabaseChanged(ListChangeListener.Change<? extends BibEntry> cha
272272
// Nothing to do, as permutation doesn't change matched entries
273273
} else if (change.wasUpdated()) {
274274
for (BibEntry changedEntry : change.getList().subList(change.getFrom(), change.getTo())) {
275-
if (groupNode.matches(changedEntry)) {
275+
if (isMatchEffective(this, changedEntry)) {
276276
// ADR-0038
277277
matchedEntries.add(changedEntry.getId());
278278
} else {
@@ -286,7 +286,7 @@ private void onDatabaseChanged(ListChangeListener.Change<? extends BibEntry> cha
286286
matchedEntries.remove(removedEntry.getId());
287287
}
288288
for (BibEntry addedEntry : change.getAddedSubList()) {
289-
if (groupNode.matches(addedEntry)) {
289+
if (isMatchEffective(this, addedEntry)) {
290290
// ADR-0038
291291
matchedEntries.add(addedEntry.getId());
292292
}
@@ -312,7 +312,9 @@ private void updateMatchedEntries() {
312312
// for example, a previously matched entry gets removed -> hits = hits - 1
313313
if (preferences.getGroupsPreferences().shouldDisplayGroupCount()) {
314314
BackgroundTask
315-
.wrap(() -> groupNode.findMatches(databaseContext.getDatabase()))
315+
.wrap(() -> databaseContext.getDatabase().getEntries().stream()
316+
.filter(e -> isMatchEffective(this, e))
317+
.toList())
316318
.onSuccess(entries -> {
317319
matchedEntries.clear();
318320
// ADR-0038
@@ -582,6 +584,49 @@ public boolean isEditable() {
582584
};
583585
}
584586

587+
/**
588+
* Returns whether the given entry should be considered part of this group
589+
* for the purpose of the groups sidebar (hit counter, highlighting, etc.).
590+
*
591+
* We cannot simply use groupNode.matches(entry) here. That only checks
592+
* the rule of this single group and ignores the configured hierarchy type and
593+
* any child groups created in the view model (for example automatic subgroups)
594+
*
595+
* This method applies the hierarchy type:
596+
* INDEPENDENT: match this group only,
597+
* INCLUDING: match this group or any child group,
598+
* REFINING: match this group and all ancestor groups.
599+
*/
600+
private boolean isMatchEffective(GroupNodeViewModel vm, BibEntry entry) {
601+
GroupTreeNode node = vm.groupNode;
602+
return switch (node.getGroup().getHierarchicalContext()) {
603+
case INDEPENDENT ->
604+
node.matches(entry);
605+
606+
case INCLUDING -> {
607+
if (node.matches(entry)) {
608+
yield true;
609+
}
610+
// recursively check VM-children (including auto-groups)
611+
yield vm.children.stream().anyMatch(childVm -> isMatchEffective(childVm, entry));
612+
}
613+
614+
case REFINING -> {
615+
if (!node.matches(entry)) {
616+
yield false;
617+
}
618+
var parent = node.getParent();
619+
while (parent.isPresent()) {
620+
if (!parent.get().matches(entry)) {
621+
yield false;
622+
}
623+
parent = parent.get().getParent();
624+
}
625+
yield true;
626+
}
627+
};
628+
}
629+
585630
class SearchIndexListener {
586631
@Subscribe
587632
public void listen(IndexStartedEvent event) {
@@ -603,7 +648,7 @@ public void listen(IndexAddedOrUpdatedEvent event) {
603648
}
604649
}).onFinished(() -> {
605650
for (BibEntry entry : event.entries()) {
606-
if (groupNode.matches(entry)) {
651+
if (GroupNodeViewModel.this.isMatchEffective(GroupNodeViewModel.this, entry)) {
607652
matchedEntries.add(entry.getId());
608653
} else {
609654
matchedEntries.remove(entry.getId());

jabgui/src/main/java/org/jabref/gui/maintable/MainTable.java

Lines changed: 23 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -314,11 +314,12 @@ public void clearAndSelect(BibEntry bibEntry) {
314314
// keep original entry selected and reset citation merge mode
315315
this.citationMergeMode = false;
316316
} else {
317-
// select new entry
318-
getSelectionModel().clearSelection();
319317
findEntry(bibEntry).ifPresent(entry -> {
320-
getSelectionModel().select(entry);
321-
scrollTo(entry);
318+
int index = getItems().indexOf(entry);
319+
if (index >= 0) {
320+
getSelectionModel().clearAndSelect(index);
321+
scrollTo(index);
322+
}
322323
});
323324
}
324325
}
@@ -330,16 +331,24 @@ public void clearAndSelect(List<BibEntry> bibEntries) {
330331
this.citationMergeMode = false;
331332
} else {
332333
// select new entries
333-
getSelectionModel().clearSelection();
334-
List<BibEntryTableViewModel> entries = bibEntries.stream()
335-
.filter(bibEntry -> bibEntry.getCitationKey().isPresent())
336-
.map(bibEntry -> findEntryByCitationKey(bibEntry.getCitationKey().get()))
337-
.filter(Optional::isPresent)
338-
.map(Optional::get)
339-
.toList();
340-
entries.forEach(entry -> getSelectionModel().select(entry));
341-
if (!entries.isEmpty()) {
342-
scrollTo(entries.getFirst());
334+
List<Integer> indices = bibEntries.stream()
335+
.filter(bibEntry -> bibEntry.getCitationKey().isPresent())
336+
.flatMap(bibEntry -> findEntryByCitationKey(bibEntry.getCitationKey().get()).stream())
337+
.map(entry -> getItems().indexOf(entry))
338+
.filter(index -> index >= 0)
339+
.toList();
340+
341+
if (!indices.isEmpty()) {
342+
// For multiple selections, clear once then select all
343+
getSelectionModel().clearSelection();
344+
indices.forEach(index -> {
345+
if (index < getItems().size()) {
346+
getSelectionModel().select(index);
347+
} else {
348+
LOGGER.debug("Could not select entry at index {} since it is out of bounds", index);
349+
}
350+
});
351+
scrollTo(indices.getFirst());
343352
}
344353
}
345354
}

jabgui/src/main/resources/org/jabref/gui/ai/components/aichat/chatprompt/ChatPromptComponent.fxml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,4 +18,8 @@
1818
mnemonicParsing="false"
1919
onAction="#onSendMessage"
2020
text="%Submit"/>
21+
<Button fx:id="regenerateButton"
22+
mnemonicParsing="false"
23+
onAction="#onRegenerateMessage"
24+
text="%Regenerate"/>
2125
</fx:root>

jabgui/src/test/java/org/jabref/gui/groups/GroupNodeViewModelTest.java

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -183,4 +183,44 @@ private GroupNodeViewModel getViewModelForGroup(AbstractGroup group) {
183183
private GroupNodeViewModel getViewModelForGroup(GroupTreeNode group) {
184184
return new GroupNodeViewModel(databaseContext, stateManager, taskExecutor, group, new CustomLocalDragboard(), preferences);
185185
}
186+
187+
@Test
188+
void hitsIncludingAutomaticGroupSumsVmChildren() {
189+
// Two entries that will produce VM-only subgroups under the automatic group
190+
databaseContext.getDatabase().insertEntry(new BibEntry().withField(StandardField.KEYWORDS, "A > B"));
191+
databaseContext.getDatabase().insertEntry(new BibEntry().withField(StandardField.KEYWORDS, "A > C"));
192+
193+
// Automatic group: its subgroups exist only in the ViewModel
194+
AutomaticKeywordGroup autoRoot = new AutomaticKeywordGroup(
195+
"Keywords",
196+
GroupHierarchyType.INCLUDING, // UNION behavior expected
197+
StandardField.KEYWORDS,
198+
',',
199+
'>'
200+
);
201+
202+
GroupNodeViewModel vm = getViewModelForGroup(autoRoot);
203+
204+
// INCLUDING: automatic root sums direct matches and those of its children -> expect 2.
205+
assertEquals(2, vm.getHits().getValue().intValue());
206+
}
207+
208+
@Test
209+
void hitsIndependentAutomaticGroupIgnoresVmChildren() {
210+
databaseContext.getDatabase().insertEntry(new BibEntry().withField(StandardField.KEYWORDS, "A > B"));
211+
databaseContext.getDatabase().insertEntry(new BibEntry().withField(StandardField.KEYWORDS, "A > C"));
212+
213+
AutomaticKeywordGroup autoRoot = new AutomaticKeywordGroup(
214+
"Keywords",
215+
GroupHierarchyType.INDEPENDENT, // no hierarchical aggregation
216+
StandardField.KEYWORDS,
217+
',',
218+
'>'
219+
);
220+
221+
GroupNodeViewModel vm = getViewModelForGroup(autoRoot);
222+
223+
// INDEPENDENT: automatic root has no direct matches -> expect 0.
224+
assertEquals(0, vm.getHits().getValue().intValue());
225+
}
186226
}

0 commit comments

Comments
 (0)