-
- @if (novelSuggestions === 0 && reusableSuggestions === 0) {
-
- No novel suggestions
-
- }
- @if (suggestions()?.suggested_tags?.length > 0) {
-
Tags
- @for (tag of suggestions().suggested_tags; track tag) {
-
- }
- }
- @if (suggestions()?.suggested_document_types?.length > 0) {
-
Document Types
- @for (type of suggestions().suggested_document_types; track type) {
-
- }
- }
- @if (suggestions()?.suggested_correspondents?.length > 0) {
-
Correspondents
- @for (correspondent of suggestions().suggested_correspondents; track correspondent) {
-
- }
- }
- @if (reusableSuggestions > 0) {
-
- {reusableSuggestions, plural, =1 {1 existing value suggested below} other {{{reusableSuggestions}} existing values suggested below}}
-
- }
+
+
Suggest using:
+
+
+
+
+
+
+
diff --git a/src-ui/src/app/components/common/suggestions-dropdown/suggestions-dropdown.component.scss b/src-ui/src/app/components/common/suggestions-dropdown/suggestions-dropdown.component.scss
index 19aa1dc7d..eafc734b3 100644
--- a/src-ui/src/app/components/common/suggestions-dropdown/suggestions-dropdown.component.scss
+++ b/src-ui/src/app/components/common/suggestions-dropdown/suggestions-dropdown.component.scss
@@ -1,3 +1,7 @@
.suggestions-dropdown {
min-width: 250px;
}
+
+.btn-link.dropdown-toggle::after {
+ display: none;
+}
diff --git a/src-ui/src/app/components/common/suggestions-dropdown/suggestions-dropdown.component.spec.ts b/src-ui/src/app/components/common/suggestions-dropdown/suggestions-dropdown.component.spec.ts
index 6b93d48af..f51c3132f 100644
--- a/src-ui/src/app/components/common/suggestions-dropdown/suggestions-dropdown.component.spec.ts
+++ b/src-ui/src/app/components/common/suggestions-dropdown/suggestions-dropdown.component.spec.ts
@@ -1,6 +1,7 @@
import { ComponentFixture, TestBed } from '@angular/core/testing'
import { NgbDropdownModule } from '@ng-bootstrap/ng-bootstrap'
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
+import { SuggestionSource } from 'src/app/data/ui-settings'
import { SuggestionsDropdownComponent } from './suggestions-dropdown.component'
describe('SuggestionsDropdownComponent', () => {
@@ -179,14 +180,73 @@ describe('SuggestionsDropdownComponent', () => {
it('should toggle dropdown when clickSuggest is called and suggestions are not null', () => {
fixture.componentRef.setInput('aiEnabled', true)
+ fixture.componentRef.setInput('fetchedSources', [SuggestionSource.ML])
fixture.detectChanges()
fixture.componentRef.setInput('suggestions', {
suggested_correspondents: [],
suggested_tags: [],
suggested_document_types: [],
})
+ fixture.detectChanges()
component.clickSuggest()
- expect(component.dropdown.open).toBeTruthy()
+ expect(component.dropdown.isOpen()).toBeTruthy()
expect(fixture.nativeElement.textContent).toContain('No novel suggestions')
})
+
+ it('should fetch unfetched sources and show existing suggestions', () => {
+ jest.spyOn(component.getSuggestions, 'emit')
+ fixture.componentRef.setInput('aiEnabled', true)
+ fixture.componentRef.setInput('source', SuggestionSource.Both)
+ fixture.componentRef.setInput('fetchedSources', [SuggestionSource.ML])
+ fixture.componentRef.setInput('suggestions', { tags: [1] })
+ fixture.detectChanges()
+ component.clickSuggest()
+ expect(component.getSuggestions.emit).toHaveBeenCalledWith(
+ SuggestionSource.Both
+ )
+ expect(component.dropdown.isOpen()).toBeTruthy()
+ })
+
+ it('should only show source options when AI is enabled', () => {
+ expect(
+ fixture.nativeElement.querySelector('#suggestionSourceML')
+ ).toBeNull()
+ fixture.componentRef.setInput('aiEnabled', true)
+ fixture.detectChanges()
+ fixture.nativeElement
+ .querySelector('button[title="Suggestion options"]')
+ .click()
+ fixture.detectChanges()
+ expect(
+ fixture.nativeElement.querySelector('#suggestionSourceML')
+ ).not.toBeNull()
+ })
+
+ it('should emit source changes and never allow no source', () => {
+ const emitSpy = jest.spyOn(component.sourceChange, 'emit')
+ fixture.componentRef.setInput('source', SuggestionSource.AI)
+ component.toggleSource(SuggestionSource.ML)
+ expect(emitSpy).toHaveBeenCalledWith(SuggestionSource.Both)
+
+ fixture.componentRef.setInput('source', SuggestionSource.Both)
+ component.toggleSource(SuggestionSource.AI)
+ expect(emitSpy).toHaveBeenCalledWith(SuggestionSource.ML)
+
+ emitSpy.mockClear()
+ fixture.componentRef.setInput('source', SuggestionSource.ML)
+ component.toggleSource(SuggestionSource.ML)
+ expect(emitSpy).not.toHaveBeenCalled()
+ })
+
+ it('should indicate a non-default source', () => {
+ fixture.componentRef.setInput('aiEnabled', true)
+ fixture.componentRef.setInput('source', SuggestionSource.AI)
+ fixture.componentRef.setInput('defaultSource', SuggestionSource.AI)
+ fixture.detectChanges()
+ expect(fixture.nativeElement.textContent).not.toContain('Not using default')
+
+ fixture.componentRef.setInput('source', SuggestionSource.Both)
+ fixture.detectChanges()
+ expect(fixture.nativeElement.textContent).toContain('Not using default')
+ })
})
diff --git a/src-ui/src/app/components/common/suggestions-dropdown/suggestions-dropdown.component.ts b/src-ui/src/app/components/common/suggestions-dropdown/suggestions-dropdown.component.ts
index ebb14ff79..fe41371f7 100644
--- a/src-ui/src/app/components/common/suggestions-dropdown/suggestions-dropdown.component.ts
+++ b/src-ui/src/app/components/common/suggestions-dropdown/suggestions-dropdown.component.ts
@@ -8,6 +8,7 @@ import {
import { NgbDropdown, NgbDropdownModule } from '@ng-bootstrap/ng-bootstrap'
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
import { DocumentSuggestions } from 'src/app/data/document-suggestions'
+import { SuggestionSource } from 'src/app/data/ui-settings'
import { pngxPopperOptions } from 'src/app/utils/popper-options'
@Component({
@@ -18,12 +19,16 @@ import { pngxPopperOptions } from 'src/app/utils/popper-options'
})
export class SuggestionsDropdownComponent {
public popperOptions = pngxPopperOptions
+ public readonly SuggestionSource = SuggestionSource
@ViewChild('dropdown') dropdown: NgbDropdown
readonly suggestions = input
(null)
readonly aiEnabled = input(false)
readonly loading = input(false)
readonly disabled = input(false)
+ readonly source = input(SuggestionSource.ML)
+ readonly defaultSource = input(SuggestionSource.ML)
+ readonly fetchedSources = input([])
readonly appliedTags = input([])
readonly appliedCorrespondent = input(null)
@@ -31,8 +36,10 @@ export class SuggestionsDropdownComponent {
readonly appliedStoragePath = input(null)
@Output()
- getSuggestions: EventEmitter =
- new EventEmitter()
+ getSuggestions: EventEmitter = new EventEmitter()
+
+ @Output()
+ sourceChange: EventEmitter = new EventEmitter()
@Output()
addTag: EventEmitter = new EventEmitter()
@@ -53,12 +60,45 @@ export class SuggestionsDropdownComponent {
}
if (!this.suggestions()) {
- this.getSuggestions.emit(this)
+ this.getSuggestions.emit(this.source())
+ } else if (this.hasUnfetchedSources) {
+ // sources changed, fetch the rest and show what we have meanwhile
+ this.getSuggestions.emit(this.source())
+ this.dropdown?.open()
} else {
this.dropdown?.toggle()
}
}
+ get useML(): boolean {
+ return this.source() !== SuggestionSource.AI
+ }
+
+ get useAI(): boolean {
+ return this.source() !== SuggestionSource.ML
+ }
+
+ get hasUnfetchedSources(): boolean {
+ const fetched = this.fetchedSources()
+ return (
+ (this.useML && !fetched.includes(SuggestionSource.ML)) ||
+ (this.useAI && !fetched.includes(SuggestionSource.AI))
+ )
+ }
+
+ public toggleSource(source: SuggestionSource.ML | SuggestionSource.AI) {
+ const ml = source === SuggestionSource.ML ? !this.useML : this.useML
+ const ai = source === SuggestionSource.AI ? !this.useAI : this.useAI
+ if (!ml && !ai) return
+ this.sourceChange.emit(
+ ml && ai
+ ? SuggestionSource.Both
+ : ml
+ ? SuggestionSource.ML
+ : SuggestionSource.AI
+ )
+ }
+
get novelSuggestions(): number {
return (
(this.suggestions()?.suggested_correspondents?.length ?? 0) +
diff --git a/src-ui/src/app/components/document-detail/document-detail.component.html b/src-ui/src/app/components/document-detail/document-detail.component.html
index c3f7e199d..64d0776aa 100644
--- a/src-ui/src/app/components/document-detail/document-detail.component.html
+++ b/src-ui/src/app/components/document-detail/document-detail.component.html
@@ -134,11 +134,15 @@
[loading]="suggestionsLoading()"
[suggestions]="suggestions()"
[aiEnabled]="aiEnabled"
+ [source]="suggestionSource"
+ [defaultSource]="defaultSuggestionSource"
+ [fetchedSources]="fetchedSuggestionSources()"
[appliedTags]="documentForm.value.tags"
[appliedCorrespondent]="documentForm.value.correspondent"
[appliedDocumentType]="documentForm.value.document_type"
[appliedStoragePath]="documentForm.value.storage_path"
- (getSuggestions)="getSuggestions()"
+ (getSuggestions)="getSuggestions($event)"
+ (sourceChange)="suggestionSourceOverride.set($event)"
(addTag)="createTag($event)"
(addDocumentType)="createDocumentType($event)"
(addCorrespondent)="createCorrespondent($event)">
diff --git a/src-ui/src/app/components/document-detail/document-detail.component.ts b/src-ui/src/app/components/document-detail/document-detail.component.ts
index ccb8e1bca..5b12ca98b 100644
--- a/src-ui/src/app/components/document-detail/document-detail.component.ts
+++ b/src-ui/src/app/components/document-detail/document-detail.component.ts
@@ -375,9 +375,13 @@ export class DocumentDetailComponent
return this.autoSuggestSetting()
}
+ get defaultSuggestionSource(): SuggestionSource {
+ return this.aiEnabled ? this.suggestionSourceSetting() : SuggestionSource.ML
+ }
+
get suggestionSource(): SuggestionSource {
if (!this.aiEnabled) return SuggestionSource.ML
- return this.suggestionSourceOverride() ?? this.suggestionSourceSetting()
+ return this.suggestionSourceOverride() ?? this.defaultSuggestionSource
}
get archiveContentRenderType(): ContentRenderType {