From d3710856992a91783783819217b43392aab73e00 Mon Sep 17 00:00:00 2001 From: shamoon <4887959+shamoon@users.noreply.github.com> Date: Sun, 30 Aug 2026 07:50:50 -0700 Subject: [PATCH] Tweak/fix: show existing count for ai suggestions (#13861) --- .../suggestions-dropdown.component.html | 13 ++- .../suggestions-dropdown.component.spec.ts | 102 +++++++++++++++++- .../suggestions-dropdown.component.ts | 45 +++++++- .../document-detail.component.html | 4 + 4 files changed, 155 insertions(+), 9 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 36f40f60c..ad2b72986 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 @@ -25,29 +25,34 @@
- @if (totalSuggestions === 0) { + @if (novelSuggestions === 0 && reusableSuggestions === 0) {
No novel suggestions
} - @if (suggestions()?.suggested_tags.length > 0) { + @if (suggestions()?.suggested_tags?.length > 0) { Tags @for (tag of suggestions().suggested_tags; track tag) { } } - @if (suggestions()?.suggested_document_types.length > 0) { + @if (suggestions()?.suggested_document_types?.length > 0) {
Document Types
@for (type of suggestions().suggested_document_types; track type) { } } - @if (suggestions()?.suggested_correspondents.length > 0) { + @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}} +
+ }
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 3c9328720..6b93d48af 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 @@ -21,15 +21,115 @@ describe('SuggestionsDropdownComponent', () => { fixture.detectChanges() }) - it('should calculate totalSuggestions', () => { + it('should exclude suggested storage path names from totalSuggestions', () => { fixture.componentRef.setInput('suggestions', { suggested_correspondents: ['John Doe'], suggested_tags: ['Tag1', 'Tag2'], suggested_document_types: ['Type1'], + suggested_storage_paths: ['Finance/Invoices'], }) expect(component.totalSuggestions).toBe(4) }) + it('should count suggestions when a category is absent from the response', () => { + fixture.componentRef.setInput('suggestions', { + suggested_tags: ['Tag1'], + }) + expect(component.totalSuggestions).toBe(1) + }) + + it('should count reused values the document does not have yet', () => { + fixture.componentRef.setInput('suggestions', { + tags: [1, 2, 3], + correspondents: [10], + document_types: [20], + suggested_tags: ['NewTag'], + }) + fixture.componentRef.setInput('appliedTags', [2]) + fixture.componentRef.setInput('appliedDocumentType', 20) + + // tags 1 and 3 are not applied yet, correspondent 10 is not set, tag 2 and + // document type 20 already are. + expect(component.reusableSuggestions).toBe(3) + expect(component.novelSuggestions).toBe(1) + expect(component.totalSuggestions).toBe(4) + }) + + it('should not count reused values that are already applied', () => { + fixture.componentRef.setInput('suggestions', { + tags: [1], + correspondents: [10], + document_types: [20], + }) + fixture.componentRef.setInput('appliedTags', [1]) + fixture.componentRef.setInput('appliedCorrespondent', 10) + fixture.componentRef.setInput('appliedDocumentType', 20) + + expect(component.totalSuggestions).toBe(0) + }) + + it('should point at the fields when suggestions are all reused', () => { + // The dropdown lists only values to create, so a response made entirely of + // reused existing objects used to render as "No novel suggestions". + fixture.componentRef.setInput('aiEnabled', true) + fixture.componentRef.setInput('suggestions', { + tags: [1, 2], + suggested_tags: [], + suggested_correspondents: [], + suggested_document_types: [], + }) + fixture.detectChanges() + component.clickSuggest() + fixture.detectChanges() + + expect(fixture.nativeElement.textContent).toContain( + '2 existing values suggested below' + ) + expect(fixture.nativeElement.textContent).not.toContain( + 'No novel suggestions' + ) + }) + + it('should account for reused values alongside values to create', () => { + // The badge counts both, but only the novel names are listed here, so the + // dropdown has to say where the rest of the count came from. + fixture.componentRef.setInput('aiEnabled', true) + fixture.componentRef.setInput('suggestions', { + tags: [6, 3], + suggested_tags: ['Arbitration', 'New York'], + suggested_correspondents: [], + suggested_document_types: [], + }) + fixture.detectChanges() + component.clickSuggest() + fixture.detectChanges() + + expect(component.totalSuggestions).toBe(4) + expect(fixture.nativeElement.textContent).toContain('Arbitration') + expect(fixture.nativeElement.textContent).toContain( + '2 existing values suggested below' + ) + }) + + it('should count classic (non-AI) suggestions, which are ids only', () => { + // /api/documents/{id}/suggestions/ returns only id arrays and no + // suggested_* keys at all, so every one of its suggestions is a reused + // existing object - including storage paths. + fixture.componentRef.setInput('suggestions', { + correspondents: [4], + tags: [6, 3], + document_types: [2], + storage_paths: [7], + dates: ['2005-01-01'], + }) + + expect(component.novelSuggestions).toBe(0) + expect(component.totalSuggestions).toBe(5) + + fixture.componentRef.setInput('appliedStoragePath', 7) + expect(component.totalSuggestions).toBe(4) + }) + it('should show when a completed request returned no suggestions', () => { fixture.componentRef.setInput('suggestions', { correspondents: [], 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 cd5b85f46..ebb14ff79 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 @@ -25,6 +25,11 @@ export class SuggestionsDropdownComponent { readonly loading = input(false) readonly disabled = input(false) + readonly appliedTags = input([]) + readonly appliedCorrespondent = input(null) + readonly appliedDocumentType = input(null) + readonly appliedStoragePath = input(null) + @Output() getSuggestions: EventEmitter = new EventEmitter() @@ -54,14 +59,46 @@ export class SuggestionsDropdownComponent { } } - get totalSuggestions(): number { + get novelSuggestions(): number { return ( - this.suggestions()?.suggested_correspondents?.length + - this.suggestions()?.suggested_tags?.length + - this.suggestions()?.suggested_document_types?.length || 0 + (this.suggestions()?.suggested_correspondents?.length ?? 0) + + (this.suggestions()?.suggested_tags?.length ?? 0) + + (this.suggestions()?.suggested_document_types?.length ?? 0) ) } + get reusableSuggestions(): number { + const correspondent = this.appliedCorrespondent() + const documentType = this.appliedDocumentType() + const storagePath = this.appliedStoragePath() + // Storage paths count here but not in novelSuggestions: an existing one + // can be applied from the field, a suggested name cannot create one. + return ( + this.countUnapplied(this.suggestions()?.tags, this.appliedTags()) + + this.countUnapplied( + this.suggestions()?.correspondents, + correspondent ? [correspondent] : [] + ) + + this.countUnapplied( + this.suggestions()?.document_types, + documentType ? [documentType] : [] + ) + + this.countUnapplied( + this.suggestions()?.storage_paths, + storagePath ? [storagePath] : [] + ) + ) + } + + get totalSuggestions(): number { + return this.novelSuggestions + this.reusableSuggestions + } + + private countUnapplied(suggested: number[], applied: number[]): number { + return (suggested ?? []).filter((id) => !(applied ?? []).includes(id)) + .length + } + get noSuggestions(): boolean { const suggestions = this.suggestions() return ( 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 d3929674e..c3f7e199d 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,6 +134,10 @@ [loading]="suggestionsLoading()" [suggestions]="suggestions()" [aiEnabled]="aiEnabled" + [appliedTags]="documentForm.value.tags" + [appliedCorrespondent]="documentForm.value.correspondent" + [appliedDocumentType]="documentForm.value.document_type" + [appliedStoragePath]="documentForm.value.storage_path" (getSuggestions)="getSuggestions()" (addTag)="createTag($event)" (addDocumentType)="createDocumentType($event)"