mirror of
https://github.com/paperless-ngx/paperless-ngx.git
synced 2025-12-16 11:36:39 +01:00
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:
parent
5695d41903
commit
0fd8706337
5 changed files with 225 additions and 52 deletions
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue