From a72c6c6a92115b2c9346419b7fc8f6064768f181 Mon Sep 17 00:00:00 2001 From: shamoon <4887959+shamoon@users.noreply.github.com> Date: Thu, 24 Sep 2026 14:53:11 -0700 Subject: [PATCH] ... button for choosing which type --- .../suggestions-dropdown.component.html | 128 +++++++++++------- .../suggestions-dropdown.component.scss | 4 + .../suggestions-dropdown.component.spec.ts | 62 ++++++++- .../suggestions-dropdown.component.ts | 46 ++++++- .../document-detail.component.html | 6 +- .../document-detail.component.ts | 6 +- 6 files changed, 195 insertions(+), 57 deletions(-) diff --git a/src-ui/src/app/components/common/suggestions-dropdown/suggestions-dropdown.component.html b/src-ui/src/app/components/common/suggestions-dropdown/suggestions-dropdown.component.html index ad2b72986..d43b56fbc 100644 --- a/src-ui/src/app/components/common/suggestions-dropdown/suggestions-dropdown.component.html +++ b/src-ui/src/app/components/common/suggestions-dropdown/suggestions-dropdown.component.html @@ -1,58 +1,84 @@ -
- + + @if (aiEnabled()) { +
+ + +
+
+ @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}} +
+ } +
+
+
} - @if (noSuggestions) { - No suggestions - } @else { - Suggest - } - @if (totalSuggestions > 0) { - {{ totalSuggestions }} - } - +
@if (aiEnabled()) { -
- - -
-
- @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 {