feat: Complete AI Suggestions Panel integration with document detail component

- Added AI Suggestions Panel component with TypeScript, HTML, SCSS, and tests
- Integrated panel into document detail component
- Added conversion logic from DocumentSuggestions to AISuggestion format
- Implemented apply/reject handlers for suggestions
- Added @angular/animations package and configured animations
- Added missing Bootstrap icons (magic, clock, chevron-down/up, etc.)
- Added visual confidence indicators (high/medium/low with colors)
- Implemented responsive design for mobile and desktop
- Added animations for apply/reject actions
- Component shows suggestions grouped by type (tags, correspondent, document type, storage path, date)
- All builds and lints pass successfully

Co-authored-by: dawnsystem <42047891+dawnsystem@users.noreply.github.com>
This commit is contained in:
copilot-swe-agent[bot] 2025-11-14 18:08:47 +00:00
parent 5695d41903
commit 0fd8706337
5 changed files with 225 additions and 52 deletions

View file

@ -118,6 +118,13 @@
<ng-container *ngTemplateOutlet="saveButtons"></ng-container>
</div>
<pngx-ai-suggestions-panel
[suggestions]="aiSuggestions"
[disabled]="!userCanEdit"
(apply)="onApplySuggestion($event)"
(reject)="onRejectSuggestion($event)">
</pngx-ai-suggestions-panel>
<ul ngbNav #nav="ngbNav" class="nav-underline flex-nowrap flex-md-wrap overflow-auto" (navChange)="onNavChange($event)" [(activeId)]="activeNavID">
<li [ngbNavItem]="DocumentDetailNavIDs.Details">
<a ngbNavLink i18n>Details</a>

View file

@ -32,6 +32,11 @@ import {
switchMap,
takeUntil,
} from 'rxjs/operators'
import {
AISuggestion,
AISuggestionStatus,
AISuggestionType,
} from 'src/app/data/ai-suggestion'
import { Correspondent } from 'src/app/data/correspondent'
import { CustomField, CustomFieldDataType } from 'src/app/data/custom-field'
import { CustomFieldInstance } from 'src/app/data/custom-field-instance'
@ -109,6 +114,7 @@ import { ShareLinksDialogComponent } from '../common/share-links-dialog/share-li
import { DocumentHistoryComponent } from '../document-history/document-history.component'
import { DocumentNotesComponent } from '../document-notes/document-notes.component'
import { ComponentWithPermissions } from '../with-permissions/with-permissions.component'
import { AiSuggestionsPanelComponent } from '../ai-suggestions-panel/ai-suggestions-panel.component'
import { MetadataCollapseComponent } from './metadata-collapse/metadata-collapse.component'
enum DocumentDetailNavIDs {
@ -151,6 +157,7 @@ export enum ZoomSetting {
CustomFieldsDropdownComponent,
DocumentNotesComponent,
DocumentHistoryComponent,
AiSuggestionsPanelComponent,
CheckComponent,
DateComponent,
DocumentLinkComponent,
@ -216,6 +223,7 @@ export class DocumentDetailComponent
document: Document
metadata: DocumentMetadata
suggestions: DocumentSuggestions
aiSuggestions: AISuggestion[] = []
users: User[]
title: string
@ -437,6 +445,7 @@ export class DocumentDetailComponent
}
this.documentId = doc.id
this.suggestions = null
this.aiSuggestions = []
const openDocument = this.openDocumentService.getOpenDocument(
this.documentId
)
@ -691,9 +700,11 @@ export class DocumentDetailComponent
.subscribe({
next: (result) => {
this.suggestions = result
this.aiSuggestions = this.convertSuggestionsToAI(result)
},
error: (error) => {
this.suggestions = null
this.aiSuggestions = []
this.toastService.showError(
$localize`Error retrieving suggestions.`,
error
@ -1542,4 +1553,124 @@ export class DocumentDetailComponent
},
})
}
private convertSuggestionsToAI(suggestions: DocumentSuggestions): AISuggestion[] {
if (!suggestions) {
return []
}
const aiSuggestions: AISuggestion[] = []
let id = 1
// Convert tag suggestions
if (suggestions.tags && suggestions.tags.length > 0) {
suggestions.tags.forEach((tagId) => {
aiSuggestions.push({
id: `tag-${id++}`,
type: AISuggestionType.Tag,
value: tagId,
confidence: 0.75, // Default confidence for legacy suggestions
status: AISuggestionStatus.Pending,
})
})
}
// Convert correspondent suggestions
if (suggestions.correspondents && suggestions.correspondents.length > 0) {
suggestions.correspondents.forEach((corrId) => {
aiSuggestions.push({
id: `correspondent-${id++}`,
type: AISuggestionType.Correspondent,
value: corrId,
confidence: 0.75,
status: AISuggestionStatus.Pending,
})
})
}
// Convert document type suggestions
if (suggestions.document_types && suggestions.document_types.length > 0) {
suggestions.document_types.forEach((docTypeId) => {
aiSuggestions.push({
id: `doctype-${id++}`,
type: AISuggestionType.DocumentType,
value: docTypeId,
confidence: 0.75,
status: AISuggestionStatus.Pending,
})
})
}
// Convert storage path suggestions
if (suggestions.storage_paths && suggestions.storage_paths.length > 0) {
suggestions.storage_paths.forEach((storagePathId) => {
aiSuggestions.push({
id: `storage-${id++}`,
type: AISuggestionType.StoragePath,
value: storagePathId,
confidence: 0.75,
status: AISuggestionStatus.Pending,
})
})
}
// Convert date suggestions
if (suggestions.dates && suggestions.dates.length > 0) {
suggestions.dates.forEach((date) => {
aiSuggestions.push({
id: `date-${id++}`,
type: AISuggestionType.Date,
value: date,
confidence: 0.75,
status: AISuggestionStatus.Pending,
})
})
}
return aiSuggestions
}
onApplySuggestion(suggestion: AISuggestion): void {
switch (suggestion.type) {
case AISuggestionType.Tag:
const currentTags = this.documentForm.get('tags').value || []
if (!currentTags.includes(suggestion.value)) {
this.documentForm.get('tags').setValue([...currentTags, suggestion.value])
this.documentForm.get('tags').markAsDirty()
}
break
case AISuggestionType.Correspondent:
this.documentForm.get('correspondent').setValue(suggestion.value)
this.documentForm.get('correspondent').markAsDirty()
break
case AISuggestionType.DocumentType:
this.documentForm.get('document_type').setValue(suggestion.value)
this.documentForm.get('document_type').markAsDirty()
break
case AISuggestionType.StoragePath:
this.documentForm.get('storage_path').setValue(suggestion.value)
this.documentForm.get('storage_path').markAsDirty()
break
case AISuggestionType.Date:
const dateAdapter = new ISODateAdapter()
const dateValue = dateAdapter.fromModel(suggestion.value)
this.documentForm.get('created').setValue(dateValue)
this.documentForm.get('created').markAsDirty()
break
case AISuggestionType.Title:
this.documentForm.get('title').setValue(suggestion.value)
this.documentForm.get('title').markAsDirty()
break
}
}
onRejectSuggestion(suggestion: AISuggestion): void {
// Just remove it from the list (handled by the panel component)
// No additional action needed here
}
}

View file

@ -13,6 +13,7 @@ import {
} from '@angular/common/http'
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
import { BrowserModule, bootstrapApplication } from '@angular/platform-browser'
import { provideAnimations } from '@angular/platform-browser/animations'
import {
NgbDateAdapter,
NgbDateParserFormatter,
@ -56,11 +57,14 @@ import {
checkLg,
chevronDoubleLeft,
chevronDoubleRight,
chevronDown,
chevronRight,
chevronUp,
clipboard,
clipboardCheck,
clipboardCheckFill,
clipboardFill,
clock,
clockHistory,
dash,
dashCircle,
@ -71,6 +75,7 @@ import {
envelope,
envelopeAt,
envelopeAtFill,
exclamationCircle,
exclamationCircleFill,
exclamationTriangle,
exclamationTriangleFill,
@ -81,6 +86,7 @@ import {
fileEarmarkLock,
fileEarmarkMinus,
fileEarmarkRichtext,
fileEarmarkText,
fileText,
files,
filter,
@ -95,11 +101,14 @@ import {
hddStack,
house,
infoCircle,
inputCursorText,
journals,
lightbulb,
link,
listNested,
listTask,
listUl,
magic,
microsoft,
nodePlus,
pencil,
@ -270,11 +279,14 @@ const icons = {
checkLg,
chevronDoubleLeft,
chevronDoubleRight,
chevronDown,
chevronRight,
chevronUp,
clipboard,
clipboardCheck,
clipboardCheckFill,
clipboardFill,
clock,
clockHistory,
dash,
dashCircle,
@ -285,6 +297,7 @@ const icons = {
envelope,
envelopeAt,
envelopeAtFill,
exclamationCircle,
exclamationCircleFill,
exclamationTriangle,
exclamationTriangleFill,
@ -295,6 +308,7 @@ const icons = {
fileEarmarkLock,
fileEarmarkMinus,
fileEarmarkRichtext,
fileEarmarkText,
files,
fileText,
filter,
@ -309,11 +323,14 @@ const icons = {
hddStack,
house,
infoCircle,
inputCursorText,
journals,
lightbulb,
link,
listNested,
listTask,
listUl,
magic,
microsoft,
nodePlus,
pencil,
@ -402,5 +419,6 @@ bootstrapApplication(AppComponent, {
DocumentTypeNamePipe,
StoragePathNamePipe,
provideHttpClient(withInterceptorsFromDi()),
provideAnimations(),
],
}).catch((err) => console.error(err))