+
@if (hasNext()) {
diff --git a/src-ui/src/app/components/document-detail/document-detail.component.spec.ts b/src-ui/src/app/components/document-detail/document-detail.component.spec.ts
index b1b3650c6..198e7a7a4 100644
--- a/src-ui/src/app/components/document-detail/document-detail.component.spec.ts
+++ b/src-ui/src/app/components/document-detail/document-detail.component.spec.ts
@@ -157,6 +157,16 @@ describe('DocumentDetailComponent', () => {
{
provide: TagService,
useValue: {
+ getCachedMany: (ids: number[]) =>
+ of(
+ ids.map((id) => ({
+ id,
+ name: `Tag${id}`,
+ is_inbox_tag: true,
+ color: '#ff0000',
+ text_color: '#000000',
+ }))
+ ),
listAll: () =>
of({
count: 3,
@@ -383,8 +393,32 @@ describe('DocumentDetailComponent', () => {
currentUserCan = true
})
- it('should support creating document type', () => {
+ it('should support creating tag, remove from suggestions', () => {
initNormally()
+ component.suggestions = {
+ suggested_tags: ['Tag1', 'NewTag12'],
+ }
+ let openModal: NgbModalRef
+ modalService.activeInstances.subscribe((modal) => (openModal = modal[0]))
+ const modalSpy = jest.spyOn(modalService, 'open')
+ component.createTag('NewTag12')
+ expect(modalSpy).toHaveBeenCalled()
+ openModal.componentInstance.succeeded.next({
+ id: 12,
+ name: 'NewTag12',
+ is_inbox_tag: true,
+ color: '#ff0000',
+ text_color: '#000000',
+ })
+ expect(component.tagsInput.value).toContain(12)
+ expect(component.suggestions.suggested_tags).not.toContain('NewTag12')
+ })
+
+ it('should support creating document type, remove from suggestions', () => {
+ initNormally()
+ component.suggestions = {
+ suggested_document_types: ['DocumentType1', 'NewDocType2'],
+ }
let openModal: NgbModalRef
modalService.activeInstances.subscribe((modal) => (openModal = modal[0]))
const modalSpy = jest.spyOn(modalService, 'open')
@@ -392,10 +426,16 @@ describe('DocumentDetailComponent', () => {
expect(modalSpy).toHaveBeenCalled()
openModal.componentInstance.succeeded.next({ id: 12, name: 'NewDocType12' })
expect(component.documentForm.get('document_type').value).toEqual(12)
+ expect(component.suggestions.suggested_document_types).not.toContain(
+ 'NewDocType2'
+ )
})
- it('should support creating correspondent', () => {
+ it('should support creating correspondent, remove from suggestions', () => {
initNormally()
+ component.suggestions = {
+ suggested_correspondents: ['Correspondent1', 'NewCorrrespondent12'],
+ }
let openModal: NgbModalRef
modalService.activeInstances.subscribe((modal) => (openModal = modal[0]))
const modalSpy = jest.spyOn(modalService, 'open')
@@ -406,6 +446,9 @@ describe('DocumentDetailComponent', () => {
name: 'NewCorrrespondent12',
})
expect(component.documentForm.get('correspondent').value).toEqual(12)
+ expect(component.suggestions.suggested_correspondents).not.toContain(
+ 'NewCorrrespondent12'
+ )
})
it('should support creating storage path', () => {
@@ -996,7 +1039,7 @@ describe('DocumentDetailComponent', () => {
expect(component.document.custom_fields).toHaveLength(initialLength - 1)
expect(component.customFieldFormFields).toHaveLength(initialLength - 1)
expect(
- fixture.debugElement.query(By.css('form')).nativeElement.textContent
+ fixture.debugElement.query(By.css('form ul')).nativeElement.textContent
).not.toContain('Field 1')
const patchSpy = jest.spyOn(documentService, 'patch')
component.save(true)
@@ -1087,10 +1130,22 @@ describe('DocumentDetailComponent', () => {
it('should get suggestions', () => {
const suggestionsSpy = jest.spyOn(documentService, 'getSuggestions')
- suggestionsSpy.mockReturnValue(of({ tags: [42, 43] }))
+ suggestionsSpy.mockReturnValue(
+ of({
+ tags: [42, 43],
+ suggested_tags: [],
+ suggested_document_types: [],
+ suggested_correspondents: [],
+ })
+ )
initNormally()
expect(suggestionsSpy).toHaveBeenCalled()
- expect(component.suggestions).toEqual({ tags: [42, 43] })
+ expect(component.suggestions).toEqual({
+ tags: [42, 43],
+ suggested_tags: [],
+ suggested_document_types: [],
+ suggested_correspondents: [],
+ })
})
it('should show error if needed for get suggestions', () => {
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 165cf0cef..a78ef4b9f 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
@@ -31,6 +31,7 @@ import {
map,
switchMap,
takeUntil,
+ tap,
} from 'rxjs/operators'
import { Correspondent } from 'src/app/data/correspondent'
import { CustomField, CustomFieldDataType } from 'src/app/data/custom-field'
@@ -76,6 +77,7 @@ import { DocumentTypeService } from 'src/app/services/rest/document-type.service
import { DocumentService } from 'src/app/services/rest/document.service'
import { SavedViewService } from 'src/app/services/rest/saved-view.service'
import { StoragePathService } from 'src/app/services/rest/storage-path.service'
+import { TagService } from 'src/app/services/rest/tag.service'
import { UserService } from 'src/app/services/rest/user.service'
import { SettingsService } from 'src/app/services/settings.service'
import { ToastService } from 'src/app/services/toast.service'
@@ -89,6 +91,7 @@ import { CorrespondentEditDialogComponent } from '../common/edit-dialog/correspo
import { DocumentTypeEditDialogComponent } from '../common/edit-dialog/document-type-edit-dialog/document-type-edit-dialog.component'
import { EditDialogMode } from '../common/edit-dialog/edit-dialog.component'
import { StoragePathEditDialogComponent } from '../common/edit-dialog/storage-path-edit-dialog/storage-path-edit-dialog.component'
+import { TagEditDialogComponent } from '../common/edit-dialog/tag-edit-dialog/tag-edit-dialog.component'
import { EmailDocumentDialogComponent } from '../common/email-document-dialog/email-document-dialog.component'
import { CheckComponent } from '../common/input/check/check.component'
import { DateComponent } from '../common/input/date/date.component'
@@ -107,6 +110,7 @@ import {
PdfEditorEditMode,
} from '../common/pdf-editor/pdf-editor.component'
import { ShareLinksDialogComponent } from '../common/share-links-dialog/share-links-dialog.component'
+import { SuggestionsDropdownComponent } from '../common/suggestions-dropdown/suggestions-dropdown.component'
import { DocumentHistoryComponent } from '../document-history/document-history.component'
import { DocumentNotesComponent } from '../document-notes/document-notes.component'
import { ComponentWithPermissions } from '../with-permissions/with-permissions.component'
@@ -163,6 +167,7 @@ export enum ZoomSetting {
NumberComponent,
MonetaryComponent,
UrlComponent,
+ SuggestionsDropdownComponent,
CustomDatePipe,
FileSizePipe,
IfPermissionsDirective,
@@ -185,6 +190,7 @@ export class DocumentDetailComponent
{
private documentsService = inject(DocumentService)
private route = inject(ActivatedRoute)
+ private tagService = inject(TagService)
private correspondentService = inject(CorrespondentService)
private documentTypeService = inject(DocumentTypeService)
private router = inject(Router)
@@ -207,6 +213,8 @@ export class DocumentDetailComponent
@ViewChild('inputTitle')
titleInput: TextComponent
+ @ViewChild('tagsInput') tagsInput: TagsComponent
+
expandOriginalMetadata = false
expandArchivedMetadata = false
@@ -218,6 +226,7 @@ export class DocumentDetailComponent
document: Document
metadata: DocumentMetadata
suggestions: DocumentSuggestions
+ suggestionsLoading: boolean = false
users: User[]
title: string
@@ -277,10 +286,10 @@ export class DocumentDetailComponent
if (
element &&
element.nativeElement.offsetParent !== null &&
- this.nav?.activeId == 4
+ this.nav?.activeId == DocumentDetailNavIDs.Preview
) {
// its visible
- setTimeout(() => this.nav?.select(1))
+ setTimeout(() => this.nav?.select(DocumentDetailNavIDs.Details))
}
}
@@ -299,6 +308,10 @@ export class DocumentDetailComponent
return this.deviceDetectorService.isMobile()
}
+ get aiEnabled(): boolean {
+ return this.settings.get(SETTINGS_KEYS.AI_ENABLED)
+ }
+
get archiveContentRenderType(): ContentRenderType {
return this.document?.archived_file_name
? this.getRenderType('application/pdf')
@@ -683,25 +696,12 @@ export class DocumentDetailComponent
PermissionType.Document
)
) {
- this.documentsService
- .getSuggestions(doc.id)
- .pipe(
- first(),
- takeUntil(this.unsubscribeNotifier),
- takeUntil(this.docChangeNotifier)
- )
- .subscribe({
- next: (result) => {
- this.suggestions = result
- },
- error: (error) => {
- this.suggestions = null
- this.toastService.showError(
- $localize`Error retrieving suggestions.`,
- error
- )
- },
- })
+ this.tagService.getCachedMany(doc.tags).subscribe((tags) => {
+ // only show suggestions if document has inbox tags
+ if (tags.some((tag) => tag.is_inbox_tag)) {
+ this.getSuggestions()
+ }
+ })
}
this.title = this.documentTitlePipe.transform(doc.title)
this.prepareForm(doc)
@@ -711,6 +711,63 @@ export class DocumentDetailComponent
return this.documentForm.get('custom_fields') as FormArray
}
+ getSuggestions() {
+ this.suggestionsLoading = true
+ this.documentsService
+ .getSuggestions(this.documentId)
+ .pipe(
+ first(),
+ takeUntil(this.unsubscribeNotifier),
+ takeUntil(this.docChangeNotifier)
+ )
+ .subscribe({
+ next: (result) => {
+ this.suggestions = result
+ this.suggestionsLoading = false
+ },
+ error: (error) => {
+ this.suggestions = null
+ this.suggestionsLoading = false
+ this.toastService.showError(
+ $localize`Error retrieving suggestions.`,
+ error
+ )
+ },
+ })
+ }
+
+ createTag(newName: string) {
+ var modal = this.modalService.open(TagEditDialogComponent, {
+ backdrop: 'static',
+ })
+ modal.componentInstance.dialogMode = EditDialogMode.CREATE
+ if (newName) modal.componentInstance.object = { name: newName }
+ modal.componentInstance.succeeded
+ .pipe(
+ tap((newTag: Tag) => {
+ // remove from suggestions if present
+ if (this.suggestions) {
+ this.suggestions = {
+ ...this.suggestions,
+ suggested_tags: this.suggestions.suggested_tags.filter(
+ (tag) => tag !== newTag.name
+ ),
+ }
+ }
+ }),
+ switchMap((newTag: Tag) => {
+ return this.tagService
+ .listAll()
+ .pipe(map((tags) => ({ newTag, tags })))
+ }),
+ takeUntil(this.unsubscribeNotifier)
+ )
+ .subscribe(({ newTag, tags }) => {
+ this.tagsInput.tags = tags.results
+ this.tagsInput.addTag(newTag.id)
+ })
+ }
+
createDocumentType(newName: string) {
var modal = this.modalService.open(DocumentTypeEditDialogComponent, {
backdrop: 'static',
@@ -730,6 +787,12 @@ export class DocumentDetailComponent
this.documentTypes = documentTypes.results
this.documentForm.get('document_type').setValue(newDocumentType.id)
this.documentForm.get('document_type').markAsDirty()
+ if (this.suggestions) {
+ this.suggestions.suggested_document_types =
+ this.suggestions.suggested_document_types.filter(
+ (dt) => dt !== newName
+ )
+ }
})
}
@@ -754,6 +817,12 @@ export class DocumentDetailComponent
this.correspondents = correspondents.results
this.documentForm.get('correspondent').setValue(newCorrespondent.id)
this.documentForm.get('correspondent').markAsDirty()
+ if (this.suggestions) {
+ this.suggestions.suggested_correspondents =
+ this.suggestions.suggested_correspondents.filter(
+ (c) => c !== newName
+ )
+ }
})
}
diff --git a/src-ui/src/app/data/document-suggestions.ts b/src-ui/src/app/data/document-suggestions.ts
index 85f9f9d22..447c4402b 100644
--- a/src-ui/src/app/data/document-suggestions.ts
+++ b/src-ui/src/app/data/document-suggestions.ts
@@ -1,11 +1,17 @@
export interface DocumentSuggestions {
+ title?: string
+
tags?: number[]
+ suggested_tags?: string[]
correspondents?: number[]
+ suggested_correspondents?: string[]
document_types?: number[]
+ suggested_document_types?: string[]
storage_paths?: number[]
+ suggested_storage_paths?: string[]
dates?: string[] // ISO-formatted date string e.g. 2022-11-03
}
diff --git a/src-ui/src/app/data/paperless-config.ts b/src-ui/src/app/data/paperless-config.ts
index 3afca66ff..fd151002d 100644
--- a/src-ui/src/app/data/paperless-config.ts
+++ b/src-ui/src/app/data/paperless-config.ts
@@ -44,12 +44,24 @@ export enum ConfigOptionType {
Boolean = 'boolean',
JSON = 'json',
File = 'file',
+ Password = 'password',
}
export const ConfigCategory = {
General: $localize`General Settings`,
OCR: $localize`OCR Settings`,
Barcode: $localize`Barcode Settings`,
+ AI: $localize`AI Settings`,
+}
+
+export const LLMEmbeddingBackendConfig = {
+ OPENAI: 'openai',
+ HUGGINGFACE: 'huggingface',
+}
+
+export const LLMBackendConfig = {
+ OPENAI: 'openai',
+ OLLAMA: 'ollama',
}
export interface ConfigOption {
@@ -59,6 +71,7 @@ export interface ConfigOption {
choices?: Array<{ id: string; name: string }>
config_key?: string
category: string
+ note?: string
}
function mapToItems(enumObj: Object): Array<{ id: string; name: string }> {
@@ -258,6 +271,58 @@ export const PaperlessConfigOptions: ConfigOption[] = [
config_key: 'PAPERLESS_CONSUMER_TAG_BARCODE_MAPPING',
category: ConfigCategory.Barcode,
},
+ {
+ key: 'ai_enabled',
+ title: $localize`AI Enabled`,
+ type: ConfigOptionType.Boolean,
+ config_key: 'PAPERLESS_AI_ENABLED',
+ category: ConfigCategory.AI,
+ note: $localize`Consider privacy implications when enabling AI features, especially if using a remote model.`,
+ },
+ {
+ key: 'llm_embedding_backend',
+ title: $localize`LLM Embedding Backend`,
+ type: ConfigOptionType.Select,
+ choices: mapToItems(LLMEmbeddingBackendConfig),
+ config_key: 'PAPERLESS_AI_LLM_EMBEDDING_BACKEND',
+ category: ConfigCategory.AI,
+ },
+ {
+ key: 'llm_embedding_model',
+ title: $localize`LLM Embedding Model`,
+ type: ConfigOptionType.String,
+ config_key: 'PAPERLESS_AI_LLM_EMBEDDING_MODEL',
+ category: ConfigCategory.AI,
+ },
+ {
+ key: 'llm_backend',
+ title: $localize`LLM Backend`,
+ type: ConfigOptionType.Select,
+ choices: mapToItems(LLMBackendConfig),
+ config_key: 'PAPERLESS_AI_LLM_BACKEND',
+ category: ConfigCategory.AI,
+ },
+ {
+ key: 'llm_model',
+ title: $localize`LLM Model`,
+ type: ConfigOptionType.String,
+ config_key: 'PAPERLESS_AI_LLM_MODEL',
+ category: ConfigCategory.AI,
+ },
+ {
+ key: 'llm_api_key',
+ title: $localize`LLM API Key`,
+ type: ConfigOptionType.Password,
+ config_key: 'PAPERLESS_AI_LLM_API_KEY',
+ category: ConfigCategory.AI,
+ },
+ {
+ key: 'llm_endpoint',
+ title: $localize`LLM Endpoint`,
+ type: ConfigOptionType.String,
+ config_key: 'PAPERLESS_AI_LLM_ENDPOINT',
+ category: ConfigCategory.AI,
+ },
]
export interface PaperlessConfig extends ObjectWithId {
@@ -287,4 +352,11 @@ export interface PaperlessConfig extends ObjectWithId {
barcode_max_pages: number
barcode_enable_tag: boolean
barcode_tag_mapping: object
+ ai_enabled: boolean
+ llm_embedding_backend: string
+ llm_embedding_model: string
+ llm_backend: string
+ llm_model: string
+ llm_api_key: string
+ llm_endpoint: string
}
diff --git a/src-ui/src/app/data/paperless-task.ts b/src-ui/src/app/data/paperless-task.ts
index 1bec277eb..b30af7cdd 100644
--- a/src-ui/src/app/data/paperless-task.ts
+++ b/src-ui/src/app/data/paperless-task.ts
@@ -11,6 +11,7 @@ export enum PaperlessTaskName {
TrainClassifier = 'train_classifier',
SanityCheck = 'check_sanity',
IndexOptimize = 'index_optimize',
+ LLMIndexUpdate = 'llmindex_update',
}
export enum PaperlessTaskStatus {
diff --git a/src-ui/src/app/data/system-status.ts b/src-ui/src/app/data/system-status.ts
index 334dc54f8..7dcbffa20 100644
--- a/src-ui/src/app/data/system-status.ts
+++ b/src-ui/src/app/data/system-status.ts
@@ -7,6 +7,7 @@ export enum SystemStatusItemStatus {
OK = 'OK',
ERROR = 'ERROR',
WARNING = 'WARNING',
+ DISABLED = 'DISABLED',
}
export interface SystemStatus {
@@ -43,6 +44,9 @@ export interface SystemStatus {
sanity_check_status: SystemStatusItemStatus
sanity_check_last_run: string // ISO date string
sanity_check_error: string
+ llmindex_status: SystemStatusItemStatus
+ llmindex_last_modified: string // ISO date string
+ llmindex_error: string
}
websocket_connected?: SystemStatusItemStatus // added client-side
}
diff --git a/src-ui/src/app/data/ui-settings.ts b/src-ui/src/app/data/ui-settings.ts
index 6ace74810..e797fe9b3 100644
--- a/src-ui/src/app/data/ui-settings.ts
+++ b/src-ui/src/app/data/ui-settings.ts
@@ -76,6 +76,7 @@ export const SETTINGS_KEYS = {
GMAIL_OAUTH_URL: 'gmail_oauth_url',
OUTLOOK_OAUTH_URL: 'outlook_oauth_url',
EMAIL_ENABLED: 'email_enabled',
+ AI_ENABLED: 'ai_enabled',
}
export const SETTINGS: UiSetting[] = [
@@ -289,4 +290,9 @@ export const SETTINGS: UiSetting[] = [
type: 'string',
default: 'page-width', // ZoomSetting from 'document-detail.component'
},
+ {
+ key: SETTINGS_KEYS.AI_ENABLED,
+ type: 'boolean',
+ default: false,
+ },
]
diff --git a/src-ui/src/app/interceptors/csrf.interceptor.ts b/src-ui/src/app/interceptors/csrf.interceptor.ts
index 2f590c5eb..03a2fa7b3 100644
--- a/src-ui/src/app/interceptors/csrf.interceptor.ts
+++ b/src-ui/src/app/interceptors/csrf.interceptor.ts
@@ -4,15 +4,15 @@ import {
HttpInterceptor,
HttpRequest,
} from '@angular/common/http'
-import { Injectable, inject } from '@angular/core'
+import { inject, Injectable } from '@angular/core'
import { Meta } from '@angular/platform-browser'
import { CookieService } from 'ngx-cookie-service'
import { Observable } from 'rxjs'
@Injectable()
export class CsrfInterceptor implements HttpInterceptor {
- private cookieService = inject(CookieService)
- private meta = inject(Meta)
+ private cookieService: CookieService = inject(CookieService)
+ private meta: Meta = inject(Meta)
intercept(
request: HttpRequest,
diff --git a/src-ui/src/app/services/chat.service.spec.ts b/src-ui/src/app/services/chat.service.spec.ts
new file mode 100644
index 000000000..b8ca957cb
--- /dev/null
+++ b/src-ui/src/app/services/chat.service.spec.ts
@@ -0,0 +1,58 @@
+import {
+ HttpEventType,
+ provideHttpClient,
+ withInterceptorsFromDi,
+} from '@angular/common/http'
+import {
+ HttpTestingController,
+ provideHttpClientTesting,
+} from '@angular/common/http/testing'
+import { TestBed } from '@angular/core/testing'
+import { environment } from 'src/environments/environment'
+import { ChatService } from './chat.service'
+
+describe('ChatService', () => {
+ let service: ChatService
+ let httpMock: HttpTestingController
+
+ beforeEach(() => {
+ TestBed.configureTestingModule({
+ imports: [],
+ providers: [
+ ChatService,
+ provideHttpClient(withInterceptorsFromDi()),
+ provideHttpClientTesting(),
+ ],
+ })
+ service = TestBed.inject(ChatService)
+ httpMock = TestBed.inject(HttpTestingController)
+ })
+
+ afterEach(() => {
+ httpMock.verify()
+ })
+
+ it('should stream chat messages', (done) => {
+ const documentId = 1
+ const prompt = 'Hello, world!'
+ const mockResponse = 'Partial response text'
+ const apiUrl = `${environment.apiBaseUrl}documents/chat/`
+
+ service.streamChat(documentId, prompt).subscribe((chunk) => {
+ expect(chunk).toBe(mockResponse)
+ done()
+ })
+
+ const req = httpMock.expectOne(apiUrl)
+ expect(req.request.method).toBe('POST')
+ expect(req.request.body).toEqual({
+ document_id: documentId,
+ q: prompt,
+ })
+
+ req.event({
+ type: HttpEventType.DownloadProgress,
+ partialText: mockResponse,
+ } as any)
+ })
+})
diff --git a/src-ui/src/app/services/chat.service.ts b/src-ui/src/app/services/chat.service.ts
new file mode 100644
index 000000000..9ddfb8330
--- /dev/null
+++ b/src-ui/src/app/services/chat.service.ts
@@ -0,0 +1,46 @@
+import {
+ HttpClient,
+ HttpDownloadProgressEvent,
+ HttpEventType,
+} from '@angular/common/http'
+import { inject, Injectable } from '@angular/core'
+import { filter, map, Observable } from 'rxjs'
+import { environment } from 'src/environments/environment'
+
+export interface ChatMessage {
+ role: 'user' | 'assistant'
+ content: string
+ isStreaming?: boolean
+}
+
+@Injectable({
+ providedIn: 'root',
+})
+export class ChatService {
+ private http: HttpClient = inject(HttpClient)
+
+ streamChat(documentId: number, prompt: string): Observable {
+ return this.http
+ .post(
+ `${environment.apiBaseUrl}documents/chat/`,
+ {
+ document_id: documentId,
+ q: prompt,
+ },
+ {
+ observe: 'events',
+ reportProgress: true,
+ responseType: 'text',
+ withCredentials: true,
+ }
+ )
+ .pipe(
+ map((event) => {
+ if (event.type === HttpEventType.DownloadProgress) {
+ return (event as HttpDownloadProgressEvent).partialText!
+ }
+ }),
+ filter((chunk) => !!chunk)
+ )
+ }
+}
diff --git a/src-ui/src/environments/environment.prod.ts b/src-ui/src/environments/environment.prod.ts
index c8bb844e9..9ebf29d16 100644
--- a/src-ui/src/environments/environment.prod.ts
+++ b/src-ui/src/environments/environment.prod.ts
@@ -6,7 +6,7 @@ export const environment = {
apiVersion: '9', // match src/paperless/settings.py
appTitle: 'Paperless-ngx',
tag: 'prod',
- version: '2.20.3',
+ version: '2.20.5',
webSocketHost: window.location.host,
webSocketProtocol: window.location.protocol == 'https:' ? 'wss:' : 'ws:',
webSocketBaseUrl: base_url.pathname + 'ws/',
diff --git a/src-ui/src/main.ts b/src-ui/src/main.ts
index 7f1a39fbe..b85d8ff35 100644
--- a/src-ui/src/main.ts
+++ b/src-ui/src/main.ts
@@ -10,6 +10,7 @@ import { DatePipe, registerLocaleData } from '@angular/common'
import {
HTTP_INTERCEPTORS,
provideHttpClient,
+ withFetch,
withInterceptorsFromDi,
} from '@angular/common/http'
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
@@ -49,6 +50,7 @@ import {
caretDown,
caretUp,
chatLeftText,
+ chatSquareDots,
check,
check2All,
checkAll,
@@ -124,6 +126,7 @@ import {
sliders2Vertical,
sortAlphaDown,
sortAlphaUpAlt,
+ stars,
tag,
tagFill,
tags,
@@ -266,6 +269,7 @@ const icons = {
caretDown,
caretUp,
chatLeftText,
+ chatSquareDots,
check,
check2All,
checkAll,
@@ -341,6 +345,7 @@ const icons = {
sliders2Vertical,
sortAlphaDown,
sortAlphaUpAlt,
+ stars,
tagFill,
tag,
tags,
@@ -407,6 +412,6 @@ bootstrapApplication(AppComponent, {
CorrespondentNamePipe,
DocumentTypeNamePipe,
StoragePathNamePipe,
- provideHttpClient(withInterceptorsFromDi()),
+ provideHttpClient(withInterceptorsFromDi(), withFetch()),
],
}).catch((err) => console.error(err))
diff --git a/src-ui/src/theme.scss b/src-ui/src/theme.scss
index eacc3b4e7..6ff5f4a09 100644
--- a/src-ui/src/theme.scss
+++ b/src-ui/src/theme.scss
@@ -73,6 +73,7 @@ $form-check-radio-checked-bg-image-dark: url("data:image/svg+xml,