From 613a7429b80863c4886e98247cdfbead4145b66e Mon Sep 17 00:00:00 2001 From: Dan Percic Date: Fri, 26 Jul 2024 12:22:53 +0300 Subject: [PATCH 01/13] RED-9747 add initial ws entity log refresh --- apps/red-ui/src/app/app.module.ts | 11 +-- .../src/app/guards/if-logged-in.guard.ts | 1 + .../file-preview-screen.component.ts | 68 +++++++++++++------ .../services/annotation-actions.service.ts | 17 +++-- .../services/file-data.service.ts | 56 +++++++++++++-- .../app/services/files/entity-log.service.ts | 17 ++++- .../src/app/services/web-socket.service.ts | 60 ++++++++++++++++ libs/red-domain/src/index.ts | 1 + .../src/lib/web-socket/analysis-event.ts | 16 +++++ libs/red-domain/src/lib/web-socket/index.ts | 2 + libs/red-domain/src/lib/web-socket/topics.ts | 3 + package.json | 7 +- yarn.lock | 18 +++-- 13 files changed, 230 insertions(+), 47 deletions(-) create mode 100644 apps/red-ui/src/app/services/web-socket.service.ts create mode 100644 libs/red-domain/src/lib/web-socket/analysis-event.ts create mode 100644 libs/red-domain/src/lib/web-socket/index.ts create mode 100644 libs/red-domain/src/lib/web-socket/topics.ts diff --git a/apps/red-ui/src/app/app.module.ts b/apps/red-ui/src/app/app.module.ts index 52a8534ed..a65da1856 100644 --- a/apps/red-ui/src/app/app.module.ts +++ b/apps/red-ui/src/app/app.module.ts @@ -135,20 +135,20 @@ export const appModuleFactory = (config: AppConfig) => { features: { ANNOTATIONS: { color: 'aqua', - enabled: true, + enabled: false, level: NgxLoggerLevel.DEBUG, }, FILTERS: { enabled: false, }, TENANTS: { - enabled: true, + enabled: false, }, ROUTES: { - enabled: true, + enabled: false, }, PDF: { - enabled: true, + enabled: false, }, FILE: { enabled: false, @@ -171,6 +171,9 @@ export const appModuleFactory = (config: AppConfig) => { DOSSIERS_CHANGES: { enabled: false, }, + GUARDS: { + enabled: false, + }, }, } as ILoggerConfig, }, diff --git a/apps/red-ui/src/app/guards/if-logged-in.guard.ts b/apps/red-ui/src/app/guards/if-logged-in.guard.ts index caf15c448..52ac6bb1c 100644 --- a/apps/red-ui/src/app/guards/if-logged-in.guard.ts +++ b/apps/red-ui/src/app/guards/if-logged-in.guard.ts @@ -51,6 +51,7 @@ export function ifLoggedIn(): AsyncGuard { const jwtToken = jwtDecode(token) as JwtToken; const authTime = (jwtToken.auth_time || jwtToken.iat).toString(); localStorage.setItem('authTime', authTime); + localStorage.setItem('token', token); } } diff --git a/apps/red-ui/src/app/modules/file-preview/file-preview-screen.component.ts b/apps/red-ui/src/app/modules/file-preview/file-preview-screen.component.ts index 565b97eec..14de0b2c1 100644 --- a/apps/red-ui/src/app/modules/file-preview/file-preview-screen.component.ts +++ b/apps/red-ui/src/app/modules/file-preview/file-preview-screen.component.ts @@ -1,6 +1,8 @@ -import { ActivatedRouteSnapshot, NavigationExtras, Router, RouterLink } from '@angular/router'; +import { NgIf } from '@angular/common'; import { ChangeDetectorRef, Component, effect, NgZone, OnDestroy, OnInit, TemplateRef, ViewChild } from '@angular/core'; +import { ActivatedRouteSnapshot, NavigationExtras, Router, RouterLink } from '@angular/router'; import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker'; +import { InitialsAvatarComponent } from '@common-ui/users'; import { ComponentCanDeactivate } from '@guards/can-deactivate.guard'; import { CircleButtonComponent, @@ -18,10 +20,11 @@ import { Toaster, } from '@iqser/common-ui'; import { copyLocalStorageFiltersValues, FilterService, NestedFilter, processFilters } from '@iqser/common-ui/lib/filtering'; -import { AutoUnsubscribe, Bind, bool, List, OnAttach, OnDetach } from '@iqser/common-ui/lib/utils'; +import { AutoUnsubscribe, Bind, bool, List, log, OnAttach, OnDetach } from '@iqser/common-ui/lib/utils'; import { AnnotationWrapper } from '@models/file/annotation.wrapper'; import { ManualRedactionEntryTypes, ManualRedactionEntryWrapper } from '@models/file/manual-redaction-entry.wrapper'; -import { Dictionary, File, ViewModes } from '@red/domain'; +import { TranslateModule } from '@ngx-translate/core'; +import { AnalyseStatuses, AnalysisEvent, Dictionary, File, ViewModes, WsTopics } from '@red/domain'; import { ConfigService } from '@services/config.service'; import { DossierTemplatesService } from '@services/dossier-templates/dossier-templates.service'; import { DossiersService } from '@services/dossiers/dossiers.service'; @@ -29,10 +32,13 @@ import { FilesMapService } from '@services/files/files-map.service'; import { FilesService } from '@services/files/files.service'; import { PermissionsService } from '@services/permissions.service'; import { ReanalysisService } from '@services/reanalysis.service'; +import { WebSocketService } from '@services/web-socket.service'; +import { ProcessingIndicatorComponent } from '@shared/components/processing-indicator/processing-indicator.component'; +import { TypeFilterComponent } from '@shared/components/type-filter/type-filter.component'; import { Roles } from '@users/roles'; import { PreferencesKeys, UserPreferenceService } from '@users/user-preference.service'; import { NGXLogger } from 'ngx-logger'; -import { combineLatest, first, firstValueFrom, Observable, of, pairwise } from 'rxjs'; +import { combineLatest, first, firstValueFrom, Observable, of, pairwise, Subscription } from 'rxjs'; import { catchError, filter, map, startWith, switchMap, tap } from 'rxjs/operators'; import { byId, byPage, handleFilterDelta, hasChanges } from '../../utils'; import { AnnotationDrawService } from '../pdf-viewer/services/annotation-draw.service'; @@ -43,34 +49,29 @@ import { PdfViewer } from '../pdf-viewer/services/pdf-viewer.service'; import { ReadableRedactionsService } from '../pdf-viewer/services/readable-redactions.service'; import { ViewerHeaderService } from '../pdf-viewer/services/viewer-header.service'; import { ROTATION_ACTION_BUTTONS, ViewerEvents } from '../pdf-viewer/utils/constants'; +import { FileActionsComponent } from '../shared-dossiers/components/file-actions/file-actions.component'; +import { FileHeaderComponent } from './components/file-header/file-header.component'; +import { FilePreviewRightContainerComponent } from './components/right-container/file-preview-right-container.component'; +import { StructuredComponentManagementComponent } from './components/structured-component-management/structured-component-management.component'; +import { UserManagementComponent } from './components/user-management/user-management.component'; +import { ViewSwitchComponent } from './components/view-switch/view-switch.component'; import { AddHintDialogComponent } from './dialogs/add-hint-dialog/add-hint-dialog.component'; import { AddAnnotationDialogComponent } from './dialogs/docu-mine/add-annotation-dialog/add-annotation-dialog.component'; import { RedactTextDialogComponent } from './dialogs/redact-text-dialog/redact-text-dialog.component'; import { filePreviewScreenProviders } from './file-preview-providers'; import { AnnotationProcessingService } from './services/annotation-processing.service'; import { AnnotationsListingService } from './services/annotations-listing.service'; +import { DocumentInfoService } from './services/document-info.service'; import { FileDataService } from './services/file-data.service'; import { FilePreviewDialogService } from './services/file-preview-dialog.service'; import { FilePreviewStateService } from './services/file-preview-state.service'; import { ManualRedactionService } from './services/manual-redaction.service'; +import { MultiSelectService } from './services/multi-select.service'; import { PdfProxyService } from './services/pdf-proxy.service'; import { SkippedService } from './services/skipped.service'; import { StampService } from './services/stamp.service'; import { ViewModeService } from './services/view-mode.service'; import { RedactTextData } from './utils/dialog-types'; -import { MultiSelectService } from './services/multi-select.service'; -import { NgIf } from '@angular/common'; -import { ViewSwitchComponent } from './components/view-switch/view-switch.component'; -import { ProcessingIndicatorComponent } from '@shared/components/processing-indicator/processing-indicator.component'; -import { UserManagementComponent } from './components/user-management/user-management.component'; -import { TranslateModule } from '@ngx-translate/core'; -import { InitialsAvatarComponent } from '@common-ui/users'; -import { FileActionsComponent } from '../shared-dossiers/components/file-actions/file-actions.component'; -import { FilePreviewRightContainerComponent } from './components/right-container/file-preview-right-container.component'; -import { TypeFilterComponent } from '@shared/components/type-filter/type-filter.component'; -import { FileHeaderComponent } from './components/file-header/file-header.component'; -import { StructuredComponentManagementComponent } from './components/structured-component-management/structured-component-management.component'; -import { DocumentInfoService } from './services/document-info.service'; @Component({ templateUrl: './file-preview-screen.component.html', @@ -96,17 +97,19 @@ import { DocumentInfoService } from './services/document-info.service'; ], }) export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnInit, OnDestroy, OnAttach, OnDetach, ComponentCanDeactivate { - readonly circleButtonTypes = CircleButtonTypes; - readonly roles = Roles; - readonly fileId = this.state.fileId; - readonly dossierId = this.state.dossierId; @ViewChild('annotationFilterTemplate', { read: TemplateRef, static: false, }) private readonly _filterTemplate: TemplateRef; #loadAllAnnotationsEnabled = false; + readonly #wsConnection$: Observable; + #wsConnectionSub: Subscription; protected readonly isDocumine = getConfig().IS_DOCUMINE; + readonly circleButtonTypes = CircleButtonTypes; + readonly roles = Roles; + readonly fileId = this.state.fileId; + readonly dossierId = this.state.dossierId; constructor( readonly pdf: PdfViewer, @@ -145,13 +148,31 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni private readonly _dossierTemplatesService: DossierTemplatesService, private readonly _multiSelectService: MultiSelectService, private readonly _documentInfoService: DocumentInfoService, + private readonly _webSocketService: WebSocketService, ) { super(); effect(() => { const file = this.state.file(); - this._fileDataService.loadAnnotations(file).then(); + console.log('FILE CHANGED'); + // this._fileDataService.loadAnnotations(file).then(); }); + this.#wsConnection$ = this._webSocketService.listen(WsTopics.ANALYSIS).pipe( + log('[WS] Analysis events'), + filter(event => event.analyseStatus === AnalyseStatuses.FINISHED), + switchMap(event => this._fileDataService.updateAnnotations(this.state.file(), event.analysisNumber)), + log('[CONNNEECCCCCTIIONSSS] Annotations updated'), + ); + + const file = this.state.file(); + console.log(file); + console.log(this._fileDataService.annotations()); + if (this._fileDataService.annotations().length) { + firstValueFrom(this._fileDataService.updateAnnotations(file, file.numberOfAnalyses)).then(); + } else { + this._fileDataService.loadAnnotations(file).then(); + } + effect( () => { if (this._documentViewer.loaded()) { @@ -296,11 +317,13 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni super.ngOnDetach(); this.pdf.instance.UI.hotkeys.off('esc'); this._changeRef.markForCheck(); + this.#wsConnectionSub.unsubscribe(); } ngOnDestroy() { this.pdf.instance.UI.hotkeys.off('esc'); super.ngOnDestroy(); + this.#wsConnectionSub.unsubscribe(); } @Bind() @@ -339,6 +362,7 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni } async ngOnInit(): Promise { + this.#wsConnectionSub = this.#wsConnection$.subscribe(); this.#updateViewerPosition(); const file = this.state.file(); diff --git a/apps/red-ui/src/app/modules/file-preview/services/annotation-actions.service.ts b/apps/red-ui/src/app/modules/file-preview/services/annotation-actions.service.ts index 7673762e6..2568abb13 100644 --- a/apps/red-ui/src/app/modules/file-preview/services/annotation-actions.service.ts +++ b/apps/red-ui/src/app/modules/file-preview/services/annotation-actions.service.ts @@ -244,7 +244,6 @@ export class AnnotationActionsService { const text = annotation.AREA ? annotation.value : isImageText; const isApprover = this._permissionsService.isApprover(dossier); const dossierTemplate = this._dossierTemplatesService.find(this._state.dossierTemplateId); - const isUnprocessed = annotation.pending; const data: ResizeRedactionData = { redaction: annotation, @@ -275,7 +274,7 @@ export class AnnotationActionsService { await this.cancelResize(annotation); - const { fileId, dossierId, file } = this._state; + const { fileId, dossierId } = this._state; const request = this._manualRedactionService.resize([resizeRequest], dossierId, fileId, includeUnprocessed); return this.#processObsAndEmit(request); } @@ -321,7 +320,7 @@ export class AnnotationActionsService { } async #processObsAndEmit(obs: Observable) { - await firstValueFrom(obs).finally(() => this._fileDataService.annotationsChanged()); + await firstValueFrom(obs.pipe(log('==>>[[[CHANGES]]]'))).finally(() => this._fileDataService.annotationsChanged()); } #getFalsePositiveText(annotation: AnnotationWrapper) { @@ -443,9 +442,15 @@ export class AnnotationActionsService { // todo: might not be correct, probably shouldn't get to this point if they are not all the same const isHint = redactions.every(r => r.isHint); const { dossierId, fileId } = this._state; - this.#processObsAndEmit( - this._manualRedactionService.removeRedaction(body, dossierId, fileId, removeFromDictionary, isHint, includeUnprocessed), - ).then(); + const req$ = this._manualRedactionService.removeRedaction( + body, + dossierId, + fileId, + removeFromDictionary, + isHint, + includeUnprocessed, + ); + this.#processObsAndEmit(req$).then(() => this._fileDataService.removeAnnotations(redactions.map(r => r.id))); } #getRemoveRedactionDialog(data: RemoveRedactionData) { diff --git a/apps/red-ui/src/app/modules/file-preview/services/file-data.service.ts b/apps/red-ui/src/app/modules/file-preview/services/file-data.service.ts index 0906fb42d..12d75122b 100644 --- a/apps/red-ui/src/app/modules/file-preview/services/file-data.service.ts +++ b/apps/red-ui/src/app/modules/file-preview/services/file-data.service.ts @@ -1,6 +1,8 @@ -import { effect, inject, Injectable, Signal, signal } from '@angular/core'; +import { effect, inject, Injectable, Signal, signal, WritableSignal } from '@angular/core'; import { toObservable } from '@angular/core/rxjs-interop'; import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker'; +import { TenantsService } from '@common-ui/tenants'; +import { log } from '@common-ui/utils'; import { EntitiesService, getConfig, Toaster } from '@iqser/common-ui'; import { AnnotationWrapper } from '@models/file/annotation.wrapper'; import { @@ -27,6 +29,7 @@ import { UserPreferenceService } from '@users/user-preference.service'; import dayjs from 'dayjs'; import { NGXLogger } from 'ngx-logger'; import { firstValueFrom, Observable } from 'rxjs'; +import { switchMap, tap } from 'rxjs/operators'; import { FilePreviewStateService } from './file-preview-state.service'; import { MultiSelectService } from './multi-select.service'; import { ViewModeService } from './view-mode.service'; @@ -43,13 +46,14 @@ export function chronologicallyBy(property: (x: T) => string) { @Injectable() export class FileDataService extends EntitiesService { - readonly #annotations = signal([]); + readonly #annotations: WritableSignal; readonly #earmarks = signal>(new Map()); #originalViewedPages: ViewedPage[] = []; readonly #isDocumine = getConfig().IS_DOCUMINE; readonly #logger = inject(NGXLogger); readonly #toaster = inject(Toaster); readonly #isIqserDevMode = inject(UserPreferenceService).isIqserDevMode; + readonly #tenantsService = inject(TenantsService); protected readonly _entityClass = AnnotationWrapper; missingTypes = new Set(); readonly earmarks: Signal>; @@ -70,9 +74,17 @@ export class FileDataService extends EntitiesService(JSON.parse(localStorage.getItem(localStorageKey) || '[]')); this.annotations$ = toObservable(this.#annotations); this.annotations = this.#annotations.asReadonly(); this.earmarks = this.#earmarks.asReadonly(); + + effect(() => { + localStorage.setItem(localStorageKey, JSON.stringify(this.#annotations())); + console.log('FileDataService#annotations', this.#annotations()); + }); + effect(() => { const viewMode = this._viewModeService.viewMode(); const earmarks = ([] as AnnotationWrapper[]).concat(...this.#earmarks().values()); @@ -81,6 +93,10 @@ export class FileDataService extends EntitiesService old.filter(annotation => !id.includes(annotation.id))); + } + setEntities(entities: AnnotationWrapper[]): void { // this is a light version of setEntities to skip looping too much // used mostly for earmarks (which are usually a lot) @@ -119,7 +135,8 @@ export class FileDataService extends EntitiesService { + const notUpdated = old.filter(oldAnnotation => { + return !annotations.some(newAnnotation => newAnnotation.id === oldAnnotation.id); + }); + return [...notUpdated, ...annotations].sort((a, b) => a.positions[0].page - b.positions[0].page); + }); + } + + async processEntityLog(entityLog: IEntityLog) { + let annotations = await this.#convertData(entityLog); this.#checkMissingTypes(); - annotations = this.#isIqserDevMode ? annotations : annotations.filter(a => !a.isFalsePositive); - this.#annotations.set(annotations); + + return this.#isIqserDevMode ? annotations : annotations.filter(a => !a.isFalsePositive); + } + + updateAnnotations(file: File, analysisNumber: number) { + const delta$ = this._entityLogService.getDelta(file.dossierId, file.fileId, analysisNumber); + return delta$.pipe( + log('[REDACTION_LOG] Delta loaded'), + switchMap(delta => this.processEntityLog(delta)), + tap(annotations => { + this.#annotations.update(old => { + const notUpdated = old.filter(oldAnnotation => { + return !oldAnnotation.pending && !annotations.some(newAnnotation => newAnnotation.id === oldAnnotation.id); + }); + return [...notUpdated, ...annotations].sort((a, b) => a.positions[0].page - b.positions[0].page); + }); + }), + tap(() => this.#logger.info('[REDACTION_LOG] Annotations updated', this.#annotations())), + ); } #checkMissingTypes() { diff --git a/apps/red-ui/src/app/services/files/entity-log.service.ts b/apps/red-ui/src/app/services/files/entity-log.service.ts index dbc05ea4e..91339fd68 100644 --- a/apps/red-ui/src/app/services/files/entity-log.service.ts +++ b/apps/red-ui/src/app/services/files/entity-log.service.ts @@ -1,15 +1,15 @@ import { inject, Injectable } from '@angular/core'; -import { GenericService, isIqserDevMode, Toaster } from '@iqser/common-ui'; +import { GenericService, Toaster } from '@iqser/common-ui'; import { EntryStates, IEntityLog, IEntityLogEntry, ISectionGrid } from '@red/domain'; import { firstValueFrom, of } from 'rxjs'; -import { catchError } from 'rxjs/operators'; +import { catchError, map } from 'rxjs/operators'; @Injectable({ providedIn: 'root', }) export class EntityLogService extends GenericService { - protected readonly _defaultModelPath = ''; readonly #toaster = inject(Toaster); + protected readonly _defaultModelPath = ''; async getEntityLog(dossierId: string, fileId: string) { const queryParams = [{ key: 'includeUnprocessed', value: true }]; @@ -20,6 +20,17 @@ export class EntityLogService extends GenericService { return entityLog; } + getDelta(dossierId: string, fileId: string, analysisNumber: number) { + const req$ = this._getOne([dossierId, fileId, analysisNumber.toString()], 'entityLog'); + return req$.pipe( + map(entityLog => { + entityLog.entityLogEntry = this.#filterInvalidEntries(entityLog.entityLogEntry); + return entityLog; + }), + catchError(() => of({} as IEntityLog)), + ); + } + getSectionGrid(dossierId: string, fileId: string) { return this._getOne([dossierId, fileId], 'sectionGrid'); } diff --git a/apps/red-ui/src/app/services/web-socket.service.ts b/apps/red-ui/src/app/services/web-socket.service.ts new file mode 100644 index 000000000..feb2a015d --- /dev/null +++ b/apps/red-ui/src/app/services/web-socket.service.ts @@ -0,0 +1,60 @@ +import { inject, Injectable } from '@angular/core'; +import { TenantsService } from '@common-ui/tenants'; +import { log } from '@common-ui/utils'; +import { getConfig } from '@iqser/common-ui'; +import { IMessage, IWatchParams, RxStomp } from '@stomp/rx-stomp'; +import { StompHeaders } from '@stomp/stompjs'; + +import { NGXLogger } from 'ngx-logger'; +import { Observable } from 'rxjs'; +import { map, tap } from 'rxjs/operators'; + +@Injectable({ + providedIn: 'root', +}) +export class WebSocketService extends RxStomp { + readonly #logger = inject(NGXLogger); + readonly #config = getConfig(); + readonly #tenantService = inject(TenantsService); + + constructor() { + super(); + setTimeout(() => this.connect(), 1000); + } + + watch(opts: IWatchParams): Observable; + watch(destination: string, headers?: StompHeaders): Observable; + watch(opts: string | IWatchParams, headers?: StompHeaders): Observable { + if (typeof opts === 'string') { + return super.watch('/topic/' + this.#tenantService.activeTenantId + '/' + opts, headers); + } + + return super.watch(opts); + } + + listen(topic: string): Observable { + return this.watch(topic).pipe(map(msg => JSON.parse(msg.body))); + } + + private connect() { + const headers = { Authorization: 'Bearer ' + localStorage.getItem('token') }; + this.configure({ + debug: (msg: string) => this.#logger.debug(msg), + brokerURL: this.#config.API_URL + '/redaction-gateway-v1/websocket', + connectHeaders: headers, + }); + this.connectionState$.pipe(log('[WS] Connection state')).subscribe(); + this.webSocketErrors$.pipe(log('[WS] Errors')).subscribe(); + this.stompErrors$ + .pipe( + tap(frame => { + console.error(frame); + console.error('Broker reported error: ' + frame.headers['message']); + console.error('Additional details: ' + frame.body); + }), + ) + .subscribe(); + + this.activate(); + } +} diff --git a/libs/red-domain/src/index.ts b/libs/red-domain/src/index.ts index 34cacab3f..5459a3e63 100644 --- a/libs/red-domain/src/index.ts +++ b/libs/red-domain/src/index.ts @@ -31,3 +31,4 @@ export * from './lib/colors'; export * from './lib/component-log'; export * from './lib/component-mappings'; export * from './lib/component-definitions'; +export * from './lib/web-socket'; diff --git a/libs/red-domain/src/lib/web-socket/analysis-event.ts b/libs/red-domain/src/lib/web-socket/analysis-event.ts new file mode 100644 index 000000000..c60f416a9 --- /dev/null +++ b/libs/red-domain/src/lib/web-socket/analysis-event.ts @@ -0,0 +1,16 @@ +export const AnalyseStatuses = { + PROCESSING: 'PROCESSING', + FINISHED: 'FINISHED', +} as const; + +export type AnalyseStatus = keyof typeof AnalyseStatuses; + +export interface AnalysisEvent { + analyseStatus: AnalyseStatus; + analysisNumber: number; + dossierId: string; + fileId: string; + numberOfOCRedPages: number; + numberOfPagesToOCR: number; + timestamp: string; +} diff --git a/libs/red-domain/src/lib/web-socket/index.ts b/libs/red-domain/src/lib/web-socket/index.ts new file mode 100644 index 000000000..6a0f0ec49 --- /dev/null +++ b/libs/red-domain/src/lib/web-socket/index.ts @@ -0,0 +1,2 @@ +export * from './analysis-event'; +export * from './topics'; diff --git a/libs/red-domain/src/lib/web-socket/topics.ts b/libs/red-domain/src/lib/web-socket/topics.ts new file mode 100644 index 000000000..ed9721867 --- /dev/null +++ b/libs/red-domain/src/lib/web-socket/topics.ts @@ -0,0 +1,3 @@ +export const WsTopics = { + ANALYSIS: 'analysis-events', +} as const; diff --git a/package.json b/package.json index 1ffc8b852..f2cadfa69 100644 --- a/package.json +++ b/package.json @@ -35,12 +35,14 @@ "@ngx-translate/core": "15.0.0", "@ngx-translate/http-loader": "8.0.0", "@pdftron/webviewer": "10.10.1", + "@stomp/rx-stomp": "^2.0.0", + "@stomp/stompjs": "^7.0.0", "chart.js": "4.4.3", "dayjs": "1.11.11", "file-saver": "^2.0.5", "jszip": "^3.10.1", "jwt-decode": "^4.0.0", - "keycloak-angular": "15.1.0", + "keycloak-angular": "16.0.1", "keycloak-js": "23.0.1", "lodash-es": "^4.17.21", "monaco-editor": "0.49.0", @@ -100,5 +102,6 @@ "webpack": "5.92.0", "webpack-bundle-analyzer": "4.10.2", "xliff": "^6.2.1" - } + }, + "packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e" } diff --git a/yarn.lock b/yarn.lock index 00c5c6c4a..f7e8bddd8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3396,6 +3396,16 @@ dependencies: "@sinonjs/commons" "^3.0.0" +"@stomp/rx-stomp@^2.0.0": + version "2.0.0" + resolved "https://registry.yarnpkg.com/@stomp/rx-stomp/-/rx-stomp-2.0.0.tgz#5d75c87db280d2af9da7fccd3478c682df312065" + integrity sha512-3UxTxAA3NWGnwFfIvN8AigJ7BxGXG0u5IK8K12mQ9cCMuaT/MM7xlyZnuV8sDbHiqqLlbwA1wk1fDfUyOTIeug== + +"@stomp/stompjs@^7.0.0": + version "7.0.0" + resolved "https://registry.yarnpkg.com/@stomp/stompjs/-/stompjs-7.0.0.tgz#46b5c454a9dc8262e0b20f3b3dbacaa113993077" + integrity sha512-fGdq4wPDnSV/KyOsjq4P+zLc8MFWC3lMmP5FBgLWKPJTYcuCbAIrnRGjB7q2jHZdYCOD5vxLuFoKIYLy5/u8Pw== + "@tootallnate/once@2": version "2.0.0" resolved "https://registry.yarnpkg.com/@tootallnate/once/-/once-2.0.0.tgz#f544a148d3ab35801c1f633a7441fd87c2e484bf" @@ -7442,10 +7452,10 @@ karma-source-map-support@1.4.0: dependencies: source-map-support "^0.5.5" -keycloak-angular@15.1.0: - version "15.1.0" - resolved "https://registry.yarnpkg.com/keycloak-angular/-/keycloak-angular-15.1.0.tgz#56d25025ace2596ea8265e7158b66b2fb20054d1" - integrity sha512-9Wz1jEalUXeq3v88MMYEcFnF2GwUht1slMbDau8lpNEe0Wp9xcv5/NpMUP0RjsHKmNg8cX47BUsxL27Ypy7pmA== +keycloak-angular@16.0.1: + version "16.0.1" + resolved "https://registry.yarnpkg.com/keycloak-angular/-/keycloak-angular-16.0.1.tgz#9dd30e36d5320db35cf1bdb681be5552ba1104ce" + integrity sha512-ytkL32R/tfHEyZ3txQtgH1y0WofW/D36zTbo2agDCYUtZETq0wAQ3E/4bVDUAr6ZKwotgAnIyOORfErnvDkXng== dependencies: tslib "^2.3.1" From 35342707f0578f9c65bd9118c94b3fb47da06d12 Mon Sep 17 00:00:00 2001 From: Nicoleta Panaghiu Date: Thu, 1 Aug 2024 11:55:54 +0300 Subject: [PATCH 02/13] RED-9772: delete saved entity log on file overwrite. --- .../upload-download/services/file-upload.service.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/apps/red-ui/src/app/modules/upload-download/services/file-upload.service.ts b/apps/red-ui/src/app/modules/upload-download/services/file-upload.service.ts index c4f82e77a..b1a747827 100644 --- a/apps/red-ui/src/app/modules/upload-download/services/file-upload.service.ts +++ b/apps/red-ui/src/app/modules/upload-download/services/file-upload.service.ts @@ -15,6 +15,7 @@ import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker'; import { HeadersConfiguration } from '@iqser/common-ui/lib/utils'; import { LicenseService } from '@services/license.service'; import { LicenseFeatures } from '../../admin/screens/license/utils/constants'; +import { TenantsService } from '@common-ui/tenants'; export interface ActiveUpload { subscription: Subscription; @@ -43,6 +44,7 @@ export class FileUploadService extends GenericService impleme private readonly _errorMessageService: ErrorMessageService, private readonly _licenseService: LicenseService, private readonly _toaster: Toaster, + private readonly _tenantsService: TenantsService, ) { super(); const fileFetch$ = this.#fetchFiles$.pipe( @@ -109,6 +111,11 @@ export class FileUploadService extends GenericService impleme option = res.applyToAllFiles ? currentOption : undefined; } + const existingFile = dossierFiles.find(pf => pf.filename === file.file.name); + if (OverwriteFileOptions.FULL_OVERWRITE === currentOption || OverwriteFileOptions.PARTIAL_OVERWRITE === currentOption) { + localStorage.removeItem(`${this._tenantsService.activeTenantId}-annotations-${existingFile.id}`); + } + if (currentOption === OverwriteFileOptions.PARTIAL_OVERWRITE) { file.keepManualRedactions = true; } From e41ac70dfeb104cb5bd25cb3bc07f3b2c1b7578f Mon Sep 17 00:00:00 2001 From: Dan Percic Date: Sat, 10 Aug 2024 14:18:22 +0200 Subject: [PATCH 03/13] copilot ws implementation, wip --- .../src/app/modules/admin/admin.routes.ts | 49 +++++-- .../rules-screen/rules-screen.component.html | 66 +++++++-- .../rules-screen/rules-screen.component.scss | 15 ++ .../rules-screen/rules-screen.component.ts | 138 ++++++++++++------ .../dossier-overview-screen.component.ts | 20 +-- .../file-preview/file-preview-providers.ts | 2 +- .../file-preview/file-preview.routes.ts | 26 +++- .../src/app/services/copilot.service.ts | 11 ++ .../src/app/services/web-socket.service.ts | 18 ++- apps/red-ui/src/assets/config/config.json | 4 +- libs/common-ui | 2 +- 11 files changed, 258 insertions(+), 93 deletions(-) create mode 100644 apps/red-ui/src/app/services/copilot.service.ts diff --git a/apps/red-ui/src/app/modules/admin/admin.routes.ts b/apps/red-ui/src/app/modules/admin/admin.routes.ts index fd10a56ab..233f15870 100644 --- a/apps/red-ui/src/app/modules/admin/admin.routes.ts +++ b/apps/red-ui/src/app/modules/admin/admin.routes.ts @@ -1,22 +1,24 @@ -import { CompositeRouteGuard, IqserPermissionsGuard, IqserRoutes } from '@iqser/common-ui'; -import { RedRoleGuard } from '@users/red-role.guard'; -import { EntitiesListingScreenComponent } from './screens/entities-listing/entities-listing-screen.component'; +import { ENVIRONMENT_INITIALIZER, inject } from '@angular/core'; import { PendingChangesGuard } from '@guards/can-deactivate.guard'; -import { DefaultColorsScreenComponent } from './screens/default-colors/default-colors-screen.component'; -import { UserListingScreenComponent } from './screens/user-listing/user-listing-screen.component'; -import { DigitalSignatureScreenComponent } from './screens/digital-signature/digital-signature-screen.component'; -import { AuditScreenComponent } from './screens/audit/audit-screen.component'; -import { GeneralConfigScreenComponent } from './screens/general-config/general-config-screen.component'; +import { templateExistsWhenEnteringAdmin } from '@guards/dossier-template-exists.guard'; +import { DossierTemplatesGuard } from '@guards/dossier-templates.guard'; +import { entityExistsGuard } from '@guards/entity-exists-guard.service'; +import { PermissionsGuard } from '@guards/permissions-guard'; +import { CompositeRouteGuard, IqserPermissionsGuard, IqserRoutes } from '@iqser/common-ui'; +import { IqserAuthGuard } from '@iqser/common-ui/lib/users'; +import { DOSSIER_TEMPLATE_ID, ENTITY_TYPE } from '@red/domain'; +import { CopilotService } from '@services/copilot.service'; +import { RedRoleGuard } from '@users/red-role.guard'; +import { Roles } from '@users/roles'; import { BaseAdminScreenComponent } from './base-admin-screen/base-admin-screen.component'; import { BaseDossierTemplateScreenComponent } from './base-dossier-templates-screen/base-dossier-template-screen.component'; -import { DossierTemplatesGuard } from '@guards/dossier-templates.guard'; -import { DOSSIER_TEMPLATE_ID, ENTITY_TYPE } from '@red/domain'; -import { templateExistsWhenEnteringAdmin } from '@guards/dossier-template-exists.guard'; -import { entityExistsGuard } from '@guards/entity-exists-guard.service'; import { BaseEntityScreenComponent } from './base-entity-screen/base-entity-screen.component'; -import { PermissionsGuard } from '@guards/permissions-guard'; -import { Roles } from '@users/roles'; -import { IqserAuthGuard } from '@iqser/common-ui/lib/users'; +import { AuditScreenComponent } from './screens/audit/audit-screen.component'; +import { DefaultColorsScreenComponent } from './screens/default-colors/default-colors-screen.component'; +import { DigitalSignatureScreenComponent } from './screens/digital-signature/digital-signature-screen.component'; +import { EntitiesListingScreenComponent } from './screens/entities-listing/entities-listing-screen.component'; +import { GeneralConfigScreenComponent } from './screens/general-config/general-config-screen.component'; +import { UserListingScreenComponent } from './screens/user-listing/user-listing-screen.component'; import { AdminDialogService } from './services/admin-dialog.service'; import { AuditService } from './services/audit.service'; import { DigitalSignatureService } from './services/digital-signature.service'; @@ -78,7 +80,22 @@ const dossierTemplateIdRoutes: IqserRoutes = [ }, type: 'ENTITY', }, - providers: [RulesService], + providers: [ + RulesService, + { + provide: ENVIRONMENT_INITIALIZER, + multi: true, + useFactory: () => { + const service = inject(CopilotService); + return () => { + setTimeout(() => { + service.connect('/api/llm/llm-websocket'); + console.log('Copilot ready'); + }, 2000); + }; + }, + }, + ], }, { path: 'component-rules', diff --git a/apps/red-ui/src/app/modules/admin/screens/rules/rules-screen/rules-screen.component.html b/apps/red-ui/src/app/modules/admin/screens/rules/rules-screen/rules-screen.component.html index 82d524af4..5ade8725a 100644 --- a/apps/red-ui/src/app/modules/admin/screens/rules/rules-screen/rules-screen.component.html +++ b/apps/red-ui/src/app/modules/admin/screens/rules/rules-screen/rules-screen.component.html @@ -1,9 +1,57 @@
-
-
+
+
- +
+ + +
+
+ +
+
+ +
+
Copilot
+ +
+ +
+ @for (comment of responses$ | async; track comment) { +
+
+
+ {{ comment.date | date: 'sophisticatedDate' }} +
+ + + +
+ +
{{ comment.text }}
+
+ } + + +
+
+
+ + + +
@@ -13,15 +61,15 @@
@@ -30,11 +78,11 @@
-
+
diff --git a/apps/red-ui/src/app/modules/admin/screens/rules/rules-screen/rules-screen.component.scss b/apps/red-ui/src/app/modules/admin/screens/rules/rules-screen/rules-screen.component.scss index 49e950daf..702bc576e 100644 --- a/apps/red-ui/src/app/modules/admin/screens/rules/rules-screen/rules-screen.component.scss +++ b/apps/red-ui/src/app/modules/admin/screens/rules/rules-screen/rules-screen.component.scss @@ -81,3 +81,18 @@ ngx-monaco-editor { gap: 24px; } } + +.right-container { + display: flex; + width: 375px; + min-width: 375px; + padding: 16px 24px 16px 24px; + + &.has-scrollbar:hover { + padding-right: 13px; + } + + redaction-dossier-details { + width: 100%; + } +} diff --git a/apps/red-ui/src/app/modules/admin/screens/rules/rules-screen/rules-screen.component.ts b/apps/red-ui/src/app/modules/admin/screens/rules/rules-screen/rules-screen.component.ts index d992fb0b7..9e133bfa6 100644 --- a/apps/red-ui/src/app/modules/admin/screens/rules/rules-screen/rules-screen.component.ts +++ b/apps/red-ui/src/app/modules/admin/screens/rules/rules-screen/rules-screen.component.ts @@ -1,22 +1,29 @@ -import { ChangeDetectionStrategy, ChangeDetectorRef, Component, computed, OnInit, signal } from '@angular/core'; -import { PermissionsService } from '@services/permissions.service'; -import { IconButtonComponent, IconButtonTypes, LoadingService, Toaster } from '@iqser/common-ui'; -import { RulesService } from '../../../services/rules.service'; -import { firstValueFrom } from 'rxjs'; -import { DOSSIER_TEMPLATE_ID, DroolsKeywords, IRules } from '@red/domain'; -import { EditorThemeService } from '@services/editor-theme.service'; +import { AsyncPipe, NgForOf, NgIf, NgTemplateOutlet } from '@angular/common'; +import { ChangeDetectionStrategy, ChangeDetectorRef, Component, computed, inject, input, OnInit, signal } from '@angular/core'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { FormsModule } from '@angular/forms'; +import { MatIcon } from '@angular/material/icon'; +import { MatTooltip } from '@angular/material/tooltip'; +import { InputWithActionComponent } from '@common-ui/inputs/input-with-action/input-with-action.component'; +import { TenantsService } from '@common-ui/tenants'; +import { getCurrentUser } from '@common-ui/users'; +import { NamePipe } from '@common-ui/users/name.pipe'; import { ComponentCanDeactivate } from '@guards/can-deactivate.guard'; -import { Debounce, getParam } from '@iqser/common-ui/lib/utils'; -import { ActivatedRoute } from '@angular/router'; +import { CircleButtonComponent, IconButtonComponent, IconButtonTypes, LoadingService, Toaster } from '@iqser/common-ui'; +import { Debounce, IqserTooltipPositions } from '@iqser/common-ui/lib/utils'; +import { MonacoEditorModule } from '@materia-ui/ngx-monaco-editor'; +import { TranslateModule } from '@ngx-translate/core'; +import { DroolsKeywords, IRules } from '@red/domain'; +import { CopilotService } from '@services/copilot.service'; +import { EditorThemeService } from '@services/editor-theme.service'; +import { PermissionsService } from '@services/permissions.service'; +import { DatePipe } from '@shared/pipes/date.pipe'; +import { BehaviorSubject, firstValueFrom } from 'rxjs'; +import { RulesService } from '../../../services/rules.service'; import { rulesScreenTranslations } from '../../../translations/rules-screen-translations'; import ICodeEditor = monaco.editor.ICodeEditor; import IModelDeltaDecoration = monaco.editor.IModelDeltaDecoration; import IStandaloneEditorConstructionOptions = monaco.editor.IStandaloneEditorConstructionOptions; -import { MonacoEditorModule } from '@materia-ui/ngx-monaco-editor'; -import { MatIcon } from '@angular/material/icon'; -import { FormsModule } from '@angular/forms'; -import { NgIf } from '@angular/common'; -import { TranslateModule } from '@ngx-translate/core'; interface SyntaxError { line: number; @@ -38,11 +45,35 @@ const RULE_VALIDATION_TIMEOUT = 2000; styleUrls: ['./rules-screen.component.scss'], changeDetection: ChangeDetectionStrategy.OnPush, standalone: true, - imports: [MonacoEditorModule, MatIcon, FormsModule, IconButtonComponent, NgIf, TranslateModule], + imports: [ + MonacoEditorModule, + MatIcon, + FormsModule, + IconButtonComponent, + NgIf, + TranslateModule, + AsyncPipe, + NgTemplateOutlet, + CircleButtonComponent, + DatePipe, + InputWithActionComponent, + NamePipe, + NgForOf, + MatTooltip, + ], }) export default class RulesScreenComponent implements OnInit, ComponentCanDeactivate { + readonly #errorGlyphs = signal([]); + #codeEditor: ICodeEditor; + #decorations: string[] = []; + readonly #errors = signal([]); + #ruleValidationTimeout: number = null; + readonly #copilotService = inject(CopilotService); + readonly #currentUser = getCurrentUser(); + protected readonly collapsed = signal(true); + protected readonly IqserTooltipPositions = IqserTooltipPositions; + readonly dossierTemplateId = input.required(); readonly translations = rulesScreenTranslations; - readonly iconButtonTypes = IconButtonTypes; readonly editorOptions: IStandaloneEditorConstructionOptions = { theme: 'vs', @@ -55,15 +86,33 @@ export default class RulesScreenComponent implements OnInit, ComponentCanDeactiv initialLines: string[] = []; currentLines: string[] = []; isLeaving = false; - readonly type: IRules['ruleFileType']; - readonly #errorGlyphs = signal([]); + readonly type = input.required(); readonly numberOfErrors = computed(() => this.#errors().filter(e => !e.warning).length); readonly numberOfWarnings = computed(() => this.#errors().filter(e => e.warning).length); - readonly #dossierTemplateId = getParam(DOSSIER_TEMPLATE_ID); - #codeEditor: ICodeEditor; - #decorations: string[] = []; - #errors = signal([]); - #ruleValidationTimeout: number = null; + readonly responses$ = new BehaviorSubject<{ text: string; date: string }[]>([]); + + constructor( + readonly permissionsService: PermissionsService, + private readonly _rulesService: RulesService, + private readonly _changeDetectorRef: ChangeDetectorRef, + private readonly _toaster: Toaster, + private readonly _loadingService: LoadingService, + private readonly _editorThemeService: EditorThemeService, + ) { + const username = this.#currentUser.username; + const tenant = inject(TenantsService).activeTenantId; + this.#copilotService + .listen('/user/' + username + '/queue/' + tenant + '/rules-copilot') + .pipe(takeUntilDestroyed()) + .subscribe(response => { + console.log('WS response: ' + response); + }); + + this.#copilotService.publish({ + destination: '/app/rules-copilot', + body: JSON.stringify({ prompts: ['manageradmin'] }), + }); + } set isLeavingPage(isLeaving: boolean) { this.isLeaving = isLeaving; @@ -85,16 +134,13 @@ export default class RulesScreenComponent implements OnInit, ComponentCanDeactiv this.#closeProblemsView(); } - constructor( - readonly permissionsService: PermissionsService, - private readonly _rulesService: RulesService, - private readonly _changeDetectorRef: ChangeDetectorRef, - private readonly _toaster: Toaster, - private readonly _loadingService: LoadingService, - private readonly _editorThemeService: EditorThemeService, - private readonly _route: ActivatedRoute, - ) { - this.type = this._route.snapshot.data.type; + add(question: string) { + console.log(question); + this.responses$.next([...this.responses$.value, { text: question, date: new Date().toISOString() }]); + this.#copilotService.publish({ + destination: '/app/rules-copilot', + body: JSON.stringify({ prompts: [question] }), + }); } async ngOnInit() { @@ -142,12 +188,20 @@ export default class RulesScreenComponent implements OnInit, ComponentCanDeactiv await this.#uploadRules(); } + revert(): void { + this.currentLines = this.initialLines; + this.#decorations = this.#codeEditor?.deltaDecorations(this.#decorations, []) || []; + this.#removeErrorMarkers(); + this._changeDetectorRef.detectChanges(); + this._loadingService.stop(); + } + async #uploadRules(dryRun = false) { return firstValueFrom( this._rulesService.uploadRules({ rules: this.#getValue(), - dossierTemplateId: this.#dossierTemplateId, - ruleFileType: this.type, + dossierTemplateId: this.dossierTemplateId(), + ruleFileType: this.type(), dryRun, }), ).then( @@ -156,7 +210,7 @@ export default class RulesScreenComponent implements OnInit, ComponentCanDeactiv this.#drawErrorMarkers(errors); if (!dryRun) { await this.#initialize(); - this._toaster.success(rulesScreenTranslations[this.type]['success.generic']); + this._toaster.success(rulesScreenTranslations[this.type()]['success.generic']); } }, error => { @@ -173,20 +227,12 @@ export default class RulesScreenComponent implements OnInit, ComponentCanDeactiv this.#drawErrorMarkers(errors); this._loadingService.stop(); if (!dryRun) { - this._toaster.error(rulesScreenTranslations[this.type]['error.generic']); + this._toaster.error(rulesScreenTranslations[this.type()]['error.generic']); } }, ); } - revert(): void { - this.currentLines = this.initialLines; - this.#decorations = this.#codeEditor?.deltaDecorations(this.#decorations, []) || []; - this.#removeErrorMarkers(); - this._changeDetectorRef.detectChanges(); - this._loadingService.stop(); - } - #mapErrors(response: UploadResponse, dryRun = false) { const warnings = response.deprecatedWarnings.map(w => ({ ...w, warning: true })); if (dryRun) { @@ -296,7 +342,7 @@ export default class RulesScreenComponent implements OnInit, ComponentCanDeactiv async #initialize() { this._loadingService.start(); - await firstValueFrom(this._rulesService.download(this.#dossierTemplateId, this.type)).then( + await firstValueFrom(this._rulesService.download(this.dossierTemplateId(), this.type())).then( rules => { this.currentLines = this.initialLines = rules.rules.split('\n'); this.revert(); diff --git a/apps/red-ui/src/app/modules/dossier-overview/screen/dossier-overview-screen.component.ts b/apps/red-ui/src/app/modules/dossier-overview/screen/dossier-overview-screen.component.ts index 9e6bfb412..bbd901aaa 100644 --- a/apps/red-ui/src/app/modules/dossier-overview/screen/dossier-overview-screen.component.ts +++ b/apps/red-ui/src/app/modules/dossier-overview/screen/dossier-overview-screen.component.ts @@ -1,9 +1,11 @@ +import { AsyncPipe, NgIf } from '@angular/common'; import { Component, ElementRef, HostListener, OnDestroy, OnInit, TemplateRef, ViewChild } from '@angular/core'; import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker'; import { CircleButtonTypes, CustomError, ErrorService, + HasScrollbarDirective, IqserListingModule, IqserPermissionsService, ListingComponent, @@ -16,6 +18,7 @@ import { } from '@iqser/common-ui'; import { NestedFilter } from '@iqser/common-ui/lib/filtering'; import { getParam, OnAttach, OnDetach, shareLast } from '@iqser/common-ui/lib/utils'; +import { TranslateModule } from '@ngx-translate/core'; import { Dossier, DOSSIER_ID, @@ -26,6 +29,7 @@ import { WorkflowFileStatus, } from '@red/domain'; import { DossierTemplatesService } from '@services/dossier-templates/dossier-templates.service'; +import { DossiersCacheService } from '@services/dossiers/dossiers-cache.service'; import { DossiersService } from '@services/dossiers/dossiers.service'; import { DossierAttributesService } from '@services/entity-services/dossier-attributes.service'; import { dossiersServiceProvider } from '@services/entity-services/dossiers.service.provider'; @@ -33,6 +37,7 @@ import { FileAttributesService } from '@services/entity-services/file-attributes import { FilesMapService } from '@services/files/files-map.service'; import { FilesService } from '@services/files/files.service'; import { PermissionsService } from '@services/permissions.service'; +import { TypeFilterComponent } from '@shared/components/type-filter/type-filter.component'; import { FileUploadModel } from '@upload-download/model/file-upload.model'; import { FileDropOverlayService } from '@upload-download/services/file-drop-overlay.service'; import { FileUploadService } from '@upload-download/services/file-upload.service'; @@ -42,17 +47,13 @@ import { UserPreferenceService } from '@users/user-preference.service'; import { convertFiles, Files, handleFileDrop } from '@utils/index'; import { merge, Observable } from 'rxjs'; import { filter, skip, switchMap, tap } from 'rxjs/operators'; +import { DossierOverviewBulkActionsComponent } from '../components/bulk-actions/dossier-overview-bulk-actions.component'; +import { DossierDetailsComponent } from '../components/dossier-details/dossier-details.component'; +import { DossierOverviewScreenHeaderComponent } from '../components/screen-header/dossier-overview-screen-header.component'; +import { TableItemComponent } from '../components/table-item/table-item.component'; +import { WorkflowItemComponent } from '../components/workflow-item/workflow-item.component'; import { ConfigService } from '../config.service'; import { BulkActionsService } from '../services/bulk-actions.service'; -import { DossiersCacheService } from '@services/dossiers/dossiers-cache.service'; -import { AsyncPipe, NgIf } from '@angular/common'; -import { DossierOverviewScreenHeaderComponent } from '../components/screen-header/dossier-overview-screen-header.component'; -import { TranslateModule } from '@ngx-translate/core'; -import { WorkflowItemComponent } from '../components/workflow-item/workflow-item.component'; -import { DossierDetailsComponent } from '../components/dossier-details/dossier-details.component'; -import { DossierOverviewBulkActionsComponent } from '../components/bulk-actions/dossier-overview-bulk-actions.component'; -import { TableItemComponent } from '../components/table-item/table-item.component'; -import { TypeFilterComponent } from '@shared/components/type-filter/type-filter.component'; @Component({ templateUrl: './dossier-overview-screen.component.html', @@ -70,6 +71,7 @@ import { TypeFilterComponent } from '@shared/components/type-filter/type-filter. DossierOverviewBulkActionsComponent, TableItemComponent, TypeFilterComponent, + HasScrollbarDirective, ], }) export default class DossierOverviewScreenComponent extends ListingComponent implements OnInit, OnAttach, OnDetach, OnDestroy { diff --git a/apps/red-ui/src/app/modules/file-preview/file-preview-providers.ts b/apps/red-ui/src/app/modules/file-preview/file-preview-providers.ts index e4abcaae4..b4e5f147d 100644 --- a/apps/red-ui/src/app/modules/file-preview/file-preview-providers.ts +++ b/apps/red-ui/src/app/modules/file-preview/file-preview-providers.ts @@ -6,6 +6,7 @@ import { AnnotationActionsService } from './services/annotation-actions.service' import { AnnotationProcessingService } from './services/annotation-processing.service'; import { AnnotationReferencesService } from './services/annotation-references.service'; import { AnnotationsListingService } from './services/annotations-listing.service'; +import { ComponentLogFilterService } from './services/component-log-filter.service'; import { DocumentInfoService } from './services/document-info.service'; import { ExcludedPagesService } from './services/excluded-pages.service'; import { FileDataService } from './services/file-data.service'; @@ -16,7 +17,6 @@ import { PdfProxyService } from './services/pdf-proxy.service'; import { SkippedService } from './services/skipped.service'; import { StampService } from './services/stamp.service'; import { ViewModeService } from './services/view-mode.service'; -import { ComponentLogFilterService } from './services/component-log-filter.service'; export const filePreviewScreenProviders = [ FilterService, diff --git a/apps/red-ui/src/app/modules/file-preview/file-preview.routes.ts b/apps/red-ui/src/app/modules/file-preview/file-preview.routes.ts index ca59f7a45..95d6028c9 100644 --- a/apps/red-ui/src/app/modules/file-preview/file-preview.routes.ts +++ b/apps/red-ui/src/app/modules/file-preview/file-preview.routes.ts @@ -1,11 +1,13 @@ -import { IqserRoutes } from '@iqser/common-ui'; -import { FilePreviewScreenComponent } from './file-preview-screen.component'; +import { ENVIRONMENT_INITIALIZER, inject } from '@angular/core'; import { PendingChangesGuard } from '@guards/can-deactivate.guard'; +import { IqserRoutes } from '@iqser/common-ui'; +import { WebSocketService } from '@services/web-socket.service'; +import { FileAssignService } from '../shared-dossiers/services/file-assign.service'; +import { FilePreviewScreenComponent } from './file-preview-screen.component'; import { DocumentUnloadedGuard } from './services/document-unloaded.guard'; import { FilePreviewDialogService } from './services/file-preview-dialog.service'; import { ManualRedactionService } from './services/manual-redaction.service'; import { TablesService } from './services/tables.service'; -import { FileAssignService } from '../shared-dossiers/services/file-assign.service'; export default [ { @@ -13,6 +15,22 @@ export default [ component: FilePreviewScreenComponent, pathMatch: 'full', canDeactivate: [PendingChangesGuard, DocumentUnloadedGuard], - providers: [FilePreviewDialogService, ManualRedactionService, DocumentUnloadedGuard, TablesService, FileAssignService], + providers: [ + FilePreviewDialogService, + ManualRedactionService, + DocumentUnloadedGuard, + TablesService, + FileAssignService, + { + provide: ENVIRONMENT_INITIALIZER, + multi: true, + useFactory: () => { + const service = inject(WebSocketService); + return () => { + setTimeout(() => service.connect('/redaction-gateway-v1/websocket'), 2000); + }; + }, + }, + ], }, ] satisfies IqserRoutes; diff --git a/apps/red-ui/src/app/services/copilot.service.ts b/apps/red-ui/src/app/services/copilot.service.ts new file mode 100644 index 000000000..4b929105c --- /dev/null +++ b/apps/red-ui/src/app/services/copilot.service.ts @@ -0,0 +1,11 @@ +import { Injectable } from '@angular/core'; +import { WebSocketService } from '@services/web-socket.service'; + +@Injectable({ + providedIn: 'root', +}) +export class CopilotService extends WebSocketService { + get topicPrefix(): string { + return ''; + } +} diff --git a/apps/red-ui/src/app/services/web-socket.service.ts b/apps/red-ui/src/app/services/web-socket.service.ts index feb2a015d..ae543dc39 100644 --- a/apps/red-ui/src/app/services/web-socket.service.ts +++ b/apps/red-ui/src/app/services/web-socket.service.ts @@ -1,4 +1,5 @@ -import { inject, Injectable } from '@angular/core'; +import { DestroyRef, inject, Injectable } from '@angular/core'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { TenantsService } from '@common-ui/tenants'; import { log } from '@common-ui/utils'; import { getConfig } from '@iqser/common-ui'; @@ -16,17 +17,21 @@ export class WebSocketService extends RxStomp { readonly #logger = inject(NGXLogger); readonly #config = getConfig(); readonly #tenantService = inject(TenantsService); + readonly #destroyRef = inject(DestroyRef); constructor() { super(); - setTimeout(() => this.connect(), 1000); + } + + get topicPrefix() { + return '/topic/' + this.#tenantService.activeTenantId + '/'; } watch(opts: IWatchParams): Observable; watch(destination: string, headers?: StompHeaders): Observable; watch(opts: string | IWatchParams, headers?: StompHeaders): Observable { if (typeof opts === 'string') { - return super.watch('/topic/' + this.#tenantService.activeTenantId + '/' + opts, headers); + return super.watch(this.topicPrefix + opts, headers); } return super.watch(opts); @@ -36,13 +41,15 @@ export class WebSocketService extends RxStomp { return this.watch(topic).pipe(map(msg => JSON.parse(msg.body))); } - private connect() { + connect(url: string) { const headers = { Authorization: 'Bearer ' + localStorage.getItem('token') }; + console.log(headers); this.configure({ debug: (msg: string) => this.#logger.debug(msg), - brokerURL: this.#config.API_URL + '/redaction-gateway-v1/websocket', + brokerURL: this.#config.API_URL + url, connectHeaders: headers, }); + this.connectionState$.pipe(log('[WS] Connection state')).subscribe(); this.webSocketErrors$.pipe(log('[WS] Errors')).subscribe(); this.stompErrors$ @@ -52,6 +59,7 @@ export class WebSocketService extends RxStomp { console.error('Broker reported error: ' + frame.headers['message']); console.error('Additional details: ' + frame.body); }), + takeUntilDestroyed(this.#destroyRef), ) .subscribe(); diff --git a/apps/red-ui/src/assets/config/config.json b/apps/red-ui/src/assets/config/config.json index 5f6c0ce6e..deb14e6da 100644 --- a/apps/red-ui/src/assets/config/config.json +++ b/apps/red-ui/src/assets/config/config.json @@ -1,7 +1,7 @@ { "ADMIN_CONTACT_NAME": null, "ADMIN_CONTACT_URL": null, - "API_URL": "https://dan2.iqser.cloud", + "API_URL": "https://dan1.iqser.cloud", "APP_NAME": "RedactManager", "IS_DOCUMINE": false, "RULE_EDITOR_DEV_ONLY": false, @@ -13,7 +13,7 @@ "MAX_RETRIES_ON_SERVER_ERROR": 3, "OAUTH_CLIENT_ID": "redaction", "OAUTH_IDP_HINT": null, - "OAUTH_URL": "https://dan2.iqser.cloud/auth", + "OAUTH_URL": "https://dan1.iqser.cloud/auth", "RECENT_PERIOD_IN_HOURS": 24, "SELECTION_MODE": "structural", "MANUAL_BASE_URL": "https://docs.redactmanager.com/preview", diff --git a/libs/common-ui b/libs/common-ui index c331a6130..17943f2e8 160000 --- a/libs/common-ui +++ b/libs/common-ui @@ -1 +1 @@ -Subproject commit c331a61309dfa220a5c83228438bc138539d2045 +Subproject commit 17943f2e8dcab28450e9102d6e8b7a0b5f7227db From c4c549fe1b6d871916498cfb85fd2c7147fd6d3f Mon Sep 17 00:00:00 2001 From: Dan Percic Date: Sat, 10 Aug 2024 14:21:27 +0200 Subject: [PATCH 04/13] fix --- .../annotations-list.component.ts | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/apps/red-ui/src/app/modules/file-preview/components/annotations-list/annotations-list.component.ts b/apps/red-ui/src/app/modules/file-preview/components/annotations-list/annotations-list.component.ts index a6827d447..7a30c67f4 100644 --- a/apps/red-ui/src/app/modules/file-preview/components/annotations-list/annotations-list.component.ts +++ b/apps/red-ui/src/app/modules/file-preview/components/annotations-list/annotations-list.component.ts @@ -1,4 +1,5 @@ -import { ChangeDetectorRef, Component, computed, ElementRef, EventEmitter, Input, Output } from '@angular/core'; +import { NgForOf, NgIf } from '@angular/common'; +import { Component, computed, ElementRef, EventEmitter, Input, Output } from '@angular/core'; import { getConfig, HasScrollbarDirective } from '@iqser/common-ui'; import { FilterService } from '@iqser/common-ui/lib/filtering'; import { IqserEventTarget } from '@iqser/common-ui/lib/utils'; @@ -11,10 +12,9 @@ import { AnnotationReferencesService } from '../../services/annotation-reference import { AnnotationsListingService } from '../../services/annotations-listing.service'; import { MultiSelectService } from '../../services/multi-select.service'; import { ViewModeService } from '../../services/view-mode.service'; -import { NgForOf, NgIf } from '@angular/common'; -import { HighlightsSeparatorComponent } from '../highlights-separator/highlights-separator.component'; -import { AnnotationWrapperComponent } from '../annotation-wrapper/annotation-wrapper.component'; import { AnnotationReferencesListComponent } from '../annotation-references-list/annotation-references-list.component'; +import { AnnotationWrapperComponent } from '../annotation-wrapper/annotation-wrapper.component'; +import { HighlightsSeparatorComponent } from '../highlights-separator/highlights-separator.component'; @Component({ selector: 'redaction-annotations-list', @@ -24,8 +24,6 @@ import { AnnotationReferencesListComponent } from '../annotation-references-list imports: [NgForOf, NgIf, HighlightsSeparatorComponent, AnnotationWrapperComponent, AnnotationReferencesListComponent], }) export class AnnotationsListComponent extends HasScrollbarDirective { - @Input({ required: true }) annotations: ListItem[]; - @Output() readonly pagesPanelActive = new EventEmitter(); readonly #earmarkGroups = computed(() => { if (this._viewModeService.isEarmarks()) { return this.#getEarmarksGroups(); @@ -33,10 +31,11 @@ export class AnnotationsListComponent extends HasScrollbarDirective { return [] as EarmarkGroup[]; }); protected readonly isDocumine = getConfig().IS_DOCUMINE; + @Input({ required: true }) annotations: ListItem[]; + @Output() readonly pagesPanelActive = new EventEmitter(); constructor( protected readonly _elementRef: ElementRef, - protected readonly _changeDetector: ChangeDetectorRef, private readonly _multiSelectService: MultiSelectService, private readonly _filterService: FilterService, private readonly _userPreferenceService: UserPreferenceService, @@ -45,7 +44,7 @@ export class AnnotationsListComponent extends HasScrollbarDirective { private readonly _listingService: AnnotationsListingService, readonly annotationReferencesService: AnnotationReferencesService, ) { - super(_elementRef, _changeDetector); + super(_elementRef); } annotationClicked(annotation: AnnotationWrapper, $event: MouseEvent): void { From 6282675682cd3be388a5a14bf9653409fb246232 Mon Sep 17 00:00:00 2001 From: Dan Percic Date: Wed, 14 Aug 2024 15:40:40 +0300 Subject: [PATCH 05/13] merge fixes --- .../screens/rules/rules-screen/rules-screen.component.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/red-ui/src/app/modules/admin/screens/rules/rules-screen/rules-screen.component.html b/apps/red-ui/src/app/modules/admin/screens/rules/rules-screen/rules-screen.component.html index f81f18fb9..f2b47dab2 100644 --- a/apps/red-ui/src/app/modules/admin/screens/rules/rules-screen/rules-screen.component.html +++ b/apps/red-ui/src/app/modules/admin/screens/rules/rules-screen/rules-screen.component.html @@ -1,5 +1,5 @@
-
+
From 74b4c1a11f85503b1308f6a8ee4f59ad5a50b73f Mon Sep 17 00:00:00 2001 From: Dan Percic Date: Mon, 19 Aug 2024 10:19:53 +0300 Subject: [PATCH 06/13] reload entity log when file is assigned to current user --- .../user-management.component.html | 2 +- .../user-management.component.ts | 30 ++++++++++++++----- .../services/annotation-actions.service.ts | 2 +- .../file-actions/file-actions.component.ts | 19 +++++++----- 4 files changed, 36 insertions(+), 17 deletions(-) diff --git a/apps/red-ui/src/app/modules/file-preview/components/user-management/user-management.component.html b/apps/red-ui/src/app/modules/file-preview/components/user-management/user-management.component.html index cdb189220..4b188f4c3 100644 --- a/apps/red-ui/src/app/modules/file-preview/components/user-management/user-management.component.html +++ b/apps/red-ui/src/app/modules/file-preview/components/user-management/user-management.component.html @@ -41,7 +41,7 @@ > [{ length: 1, color: this.state.file().workflowStatus }]); readonly assignTooltip = computed(() => { @@ -72,6 +74,13 @@ export class UserManagementComponent implements OnInit, OnDestroy { readonly ngZone: NgZone, ) {} + async assignToMe(file: File) { + await this.fileAssignService.assignToMe([file]); + //TODO: check which one to call + // await firstValueFrom(this.fileDataService.updateAnnotations(file, file.numberOfAnalyses)); + await this.fileDataService.loadEntityLog(); + } + async assignReviewer(file: File, user: User | string) { const assigneeId = typeof user === 'string' ? user : user?.id; @@ -85,6 +94,11 @@ export class UserManagementComponent implements OnInit, OnDestroy { await this.filesService.setUnderApproval(file, assigneeId); } + if (assigneeId === this._currentUserId) { + // await firstValueFrom(this.fileDataService.updateAnnotations(file, file.numberOfAnalyses)); + await this.fileDataService.loadEntityLog(); + } + this.loadingService.stop(); const translateParams = { reviewerName: this.userService.getName(assigneeId), filename: file.filename }; diff --git a/apps/red-ui/src/app/modules/file-preview/services/annotation-actions.service.ts b/apps/red-ui/src/app/modules/file-preview/services/annotation-actions.service.ts index 2568abb13..c45c0ffed 100644 --- a/apps/red-ui/src/app/modules/file-preview/services/annotation-actions.service.ts +++ b/apps/red-ui/src/app/modules/file-preview/services/annotation-actions.service.ts @@ -450,7 +450,7 @@ export class AnnotationActionsService { isHint, includeUnprocessed, ); - this.#processObsAndEmit(req$).then(() => this._fileDataService.removeAnnotations(redactions.map(r => r.id))); + this.#processObsAndEmit(req$).then(); } #getRemoveRedactionDialog(data: RemoveRedactionData) { diff --git a/apps/red-ui/src/app/modules/shared-dossiers/components/file-actions/file-actions.component.ts b/apps/red-ui/src/app/modules/shared-dossiers/components/file-actions/file-actions.component.ts index 19a721f98..f53228d4f 100644 --- a/apps/red-ui/src/app/modules/shared-dossiers/components/file-actions/file-actions.component.ts +++ b/apps/red-ui/src/app/modules/shared-dossiers/components/file-actions/file-actions.component.ts @@ -1,7 +1,9 @@ -import { ChangeDetectorRef, Component, HostBinding, Injector, Input, OnChanges, Optional, ViewChild } from '@angular/core'; +import { NgIf, NgTemplateOutlet } from '@angular/common'; +import { ChangeDetectorRef, Component, HostBinding, inject, Injector, Input, OnChanges, Optional, ViewChild } from '@angular/core'; import { toObservable } from '@angular/core/rxjs-interop'; import { Router } from '@angular/router'; import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker'; +import { StatusBarComponent } from '@common-ui/shared'; import { CircleButtonTypes, getConfig, @@ -21,6 +23,7 @@ import { FilesService } from '@services/files/files.service'; import { PermissionsService } from '@services/permissions.service'; import { ReanalysisService, ReanalyzeQueryParams } from '@services/reanalysis.service'; import { ExpandableFileActionsComponent } from '@shared/components/expandable-file-actions/expandable-file-actions.component'; +import { ProcessingIndicatorComponent } from '@shared/components/processing-indicator/processing-indicator.component'; import { LongPressDirective, LongPressEvent } from '@shared/directives/long-press.directive'; import { Roles } from '@users/roles'; import { UserPreferenceService } from '@users/user-preference.service'; @@ -28,14 +31,12 @@ import { setLocalStorageDataByFileId } from '@utils/local-storage'; import { firstValueFrom, Observable } from 'rxjs'; import { DocumentInfoService } from '../../../file-preview/services/document-info.service'; import { ExcludedPagesService } from '../../../file-preview/services/excluded-pages.service'; +import { FileDataService } from '../../../file-preview/services/file-data.service'; import { PageRotationService } from '../../../pdf-viewer/services/page-rotation.service'; import { ViewerHeaderService } from '../../../pdf-viewer/services/viewer-header.service'; import { AssignReviewerApproverDialogComponent } from '../../dialogs/assign-reviewer-approver-dialog/assign-reviewer-approver-dialog.component'; import { DossiersDialogService } from '../../services/dossiers-dialog.service'; import { FileAssignService } from '../../services/file-assign.service'; -import { ProcessingIndicatorComponent } from '@shared/components/processing-indicator/processing-indicator.component'; -import { StatusBarComponent } from '@common-ui/shared'; -import { NgIf, NgTemplateOutlet } from '@angular/common'; @Component({ selector: 'redaction-file-actions', @@ -45,6 +46,10 @@ import { NgIf, NgTemplateOutlet } from '@angular/common'; imports: [ProcessingIndicatorComponent, StatusBarComponent, LongPressDirective, ExpandableFileActionsComponent, NgTemplateOutlet, NgIf], }) export class FileActionsComponent implements OnChanges { + @ViewChild(ExpandableFileActionsComponent) + private readonly _expandableActionsComponent: ExpandableFileActionsComponent; + readonly #isDocumine = getConfig().IS_DOCUMINE; + readonly #fileDataService = inject(FileDataService, { optional: true }); @Input({ required: true }) file: File; @Input({ required: true }) dossier: Dossier; @Input({ required: true }) type: 'file-preview' | 'dossier-overview-list' | 'dossier-overview-workflow'; @@ -82,9 +87,6 @@ export class FileActionsComponent implements OnChanges { isDossierMember = false; tooltipPosition = IqserTooltipPositions.above; buttons: Action[]; - @ViewChild(ExpandableFileActionsComponent) - private readonly _expandableActionsComponent: ExpandableFileActionsComponent; - readonly #isDocumine = getConfig().IS_DOCUMINE; constructor( private readonly _injector: Injector, @@ -379,6 +381,9 @@ export class FileActionsComponent implements OnChanges { async #assignToMe() { await this._fileAssignService.assignToMe([this.file]); + // TODO: check which one to call + // await firstValueFrom(this.#fileDataService?.updateAnnotations(this.file, this.file.numberOfAnalyses)); + await this.#fileDataService?.loadEntityLog(); } async #reanalyseFile() { From 5d7849be451c1bb95ce83483f1ebb5795bf734b6 Mon Sep 17 00:00:00 2001 From: Dan Percic Date: Mon, 19 Aug 2024 10:21:49 +0300 Subject: [PATCH 07/13] filter removed entries --- .../file-preview/services/file-data.service.ts | 14 +++----------- .../src/app/services/files/entity-log.service.ts | 10 +++++----- 2 files changed, 8 insertions(+), 16 deletions(-) diff --git a/apps/red-ui/src/app/modules/file-preview/services/file-data.service.ts b/apps/red-ui/src/app/modules/file-preview/services/file-data.service.ts index 12d75122b..423fba710 100644 --- a/apps/red-ui/src/app/modules/file-preview/services/file-data.service.ts +++ b/apps/red-ui/src/app/modules/file-preview/services/file-data.service.ts @@ -93,10 +93,6 @@ export class FileDataService extends EntitiesService old.filter(annotation => !id.includes(annotation.id))); - } - setEntities(entities: AnnotationWrapper[]): void { // this is a light version of setEntities to skip looping too much // used mostly for earmarks (which are usually a lot) @@ -154,12 +150,7 @@ export class FileDataService extends EntitiesService { - const notUpdated = old.filter(oldAnnotation => { - return !annotations.some(newAnnotation => newAnnotation.id === oldAnnotation.id); - }); - return [...notUpdated, ...annotations].sort((a, b) => a.positions[0].page - b.positions[0].page); - }); + this.#annotations.set(annotations); } async processEntityLog(entityLog: IEntityLog) { @@ -175,11 +166,12 @@ export class FileDataService extends EntitiesService this.processEntityLog(delta)), tap(annotations => { + const notDeleted = annotations.filter(annotation => !annotation.isRemoved); this.#annotations.update(old => { const notUpdated = old.filter(oldAnnotation => { return !oldAnnotation.pending && !annotations.some(newAnnotation => newAnnotation.id === oldAnnotation.id); }); - return [...notUpdated, ...annotations].sort((a, b) => a.positions[0].page - b.positions[0].page); + return [...notUpdated, ...notDeleted].sort((a, b) => a.positions[0].page - b.positions[0].page); }); }), tap(() => this.#logger.info('[REDACTION_LOG] Annotations updated', this.#annotations())), diff --git a/apps/red-ui/src/app/services/files/entity-log.service.ts b/apps/red-ui/src/app/services/files/entity-log.service.ts index 91339fd68..1d1d6ac92 100644 --- a/apps/red-ui/src/app/services/files/entity-log.service.ts +++ b/apps/red-ui/src/app/services/files/entity-log.service.ts @@ -1,6 +1,6 @@ import { inject, Injectable } from '@angular/core'; import { GenericService, Toaster } from '@iqser/common-ui'; -import { EntryStates, IEntityLog, IEntityLogEntry, ISectionGrid } from '@red/domain'; +import { EntryState, EntryStates, IEntityLog, IEntityLogEntry, ISectionGrid } from '@red/domain'; import { firstValueFrom, of } from 'rxjs'; import { catchError, map } from 'rxjs/operators'; @@ -15,7 +15,7 @@ export class EntityLogService extends GenericService { const queryParams = [{ key: 'includeUnprocessed', value: true }]; const entityLog$ = this._getOne([dossierId, fileId], 'entityLog', queryParams); const entityLog = await firstValueFrom(entityLog$.pipe(catchError(() => of({} as IEntityLog)))); - entityLog.entityLogEntry = this.#filterInvalidEntries(entityLog.entityLogEntry); + entityLog.entityLogEntry = this.#filterInvalidEntries(entityLog.entityLogEntry, [EntryStates.REMOVED]); entityLog.entityLogEntry.sort((a, b) => a.positions[0].pageNumber - b.positions[0].pageNumber); return entityLog; } @@ -35,15 +35,15 @@ export class EntityLogService extends GenericService { return this._getOne([dossierId, fileId], 'sectionGrid'); } - #filterInvalidEntries(entityLogEntry: IEntityLogEntry[]) { + #filterInvalidEntries(entityLogEntry: IEntityLogEntry[], invalidStates: EntryState[] = []) { return entityLogEntry.filter(entry => { entry.positions = entry.positions?.filter(p => !!p.rectangle?.length); const hasPositions = !!entry.positions?.length; - const isRemoved = entry.state === EntryStates.REMOVED; + const hasInvalidState = invalidStates.includes(entry.state); if (!hasPositions) { this.#toaster.devInfo(`Entry ${entry.id} was skipped because it has no position`); } - return hasPositions && !isRemoved; + return hasPositions && !hasInvalidState; }); } } From 9317f55d819c24f098f92c7677a5b47c507879e9 Mon Sep 17 00:00:00 2001 From: Dan Percic Date: Mon, 19 Aug 2024 12:01:09 +0300 Subject: [PATCH 08/13] fix delete annotation --- .../annotation-actions.component.ts | 30 +++++++++---------- .../services/file-data.service.ts | 12 ++++++-- .../services/annotation-manager.service.ts | 1 + 3 files changed, 26 insertions(+), 17 deletions(-) diff --git a/apps/red-ui/src/app/modules/file-preview/components/annotation-actions/annotation-actions.component.ts b/apps/red-ui/src/app/modules/file-preview/components/annotation-actions/annotation-actions.component.ts index 3e2b3f822..8c27eee07 100644 --- a/apps/red-ui/src/app/modules/file-preview/components/annotation-actions/annotation-actions.component.ts +++ b/apps/red-ui/src/app/modules/file-preview/components/annotation-actions/annotation-actions.component.ts @@ -1,7 +1,9 @@ +import { AsyncPipe, NgIf } from '@angular/common'; import { Component, computed, Input, OnChanges } from '@angular/core'; import { CircleButtonComponent, getConfig, HelpModeService, IqserAllowDirective, IqserPermissionsService } from '@iqser/common-ui'; import { AnnotationPermissions } from '@models/file/annotation.permissions'; import { AnnotationWrapper } from '@models/file/annotation.wrapper'; +import { TranslateModule } from '@ngx-translate/core'; import { PermissionsService } from '@services/permissions.service'; import { Roles } from '@users/roles'; import { REDAnnotationManager } from '../../../pdf-viewer/services/annotation-manager.service'; @@ -9,10 +11,8 @@ import { AnnotationActionsService } from '../../services/annotation-actions.serv import { AnnotationReferencesService } from '../../services/annotation-references.service'; import { FilePreviewStateService } from '../../services/file-preview-state.service'; import { MultiSelectService } from '../../services/multi-select.service'; -import { ViewModeService } from '../../services/view-mode.service'; import { SkippedService } from '../../services/skipped.service'; -import { AsyncPipe, NgIf } from '@angular/common'; -import { TranslateModule } from '@ngx-translate/core'; +import { ViewModeService } from '../../services/view-mode.service'; export const AnnotationButtonTypes = { default: 'default', @@ -62,10 +62,6 @@ export class AnnotationActionsComponent implements OnChanges { return this.#annotations; } - get isImageHint(): boolean { - return this.annotations.every(annotation => annotation.IMAGE_HINT); - } - @Input() set annotations(annotations: AnnotationWrapper[]) { this.#annotations = annotations.filter(a => a !== undefined); @@ -73,6 +69,10 @@ export class AnnotationActionsComponent implements OnChanges { this._annotationId = this.#annotations[0]?.id; } + get isImageHint(): boolean { + return this.annotations.every(annotation => annotation.IMAGE_HINT); + } + get canEdit(): boolean { const canEditRedactions = this.annotationPermissions.canChangeLegalBasis || @@ -132,6 +132,14 @@ export class AnnotationActionsComponent implements OnChanges { return this.annotations.every(a => a.superType === type); } + get #annotationChangesAllowed() { + return (!this.#isDocumine || !this._state.file().excludedFromAutomaticAnalysis) && !this.#somePending; + } + + get #somePending() { + return this.#annotations.some(a => a.pending); + } + ngOnChanges(): void { this.#setPermissions(); } @@ -183,12 +191,4 @@ export class AnnotationActionsComponent implements OnChanges { this._state.file().excludedFromAutomaticAnalysis, ); } - - get #annotationChangesAllowed() { - return (!this.#isDocumine || !this._state.file().excludedFromAutomaticAnalysis) && !this.#somePending; - } - - get #somePending() { - return this.#annotations.some(a => a.pending); - } } diff --git a/apps/red-ui/src/app/modules/file-preview/services/file-data.service.ts b/apps/red-ui/src/app/modules/file-preview/services/file-data.service.ts index 423fba710..99a98fe76 100644 --- a/apps/red-ui/src/app/modules/file-preview/services/file-data.service.ts +++ b/apps/red-ui/src/app/modules/file-preview/services/file-data.service.ts @@ -75,7 +75,14 @@ export class FileDataService extends EntitiesService(JSON.parse(localStorage.getItem(localStorageKey) || '[]')); + const storedAnnotations = JSON.parse(localStorage.getItem(localStorageKey) || '[]') as []; + this.#annotations = signal( + storedAnnotations.map(a => { + const newAnn = new AnnotationWrapper(); + Object.assign(newAnn, a); + return newAnn; + }), + ); this.annotations$ = toObservable(this.#annotations); this.annotations = this.#annotations.asReadonly(); this.earmarks = this.#earmarks.asReadonly(); @@ -226,7 +233,8 @@ export class FileDataService extends EntitiesService) { + annotations = annotations ?? []; return annotations.map((item: string | AnnotationWrapper) => this.#getById(item)).filter(a => !!a); } From eb9bd777e05a296b58f38ab90ab97a988cafe9a7 Mon Sep 17 00:00:00 2001 From: Dan Percic Date: Mon, 19 Aug 2024 12:03:29 +0300 Subject: [PATCH 09/13] fix merge --- .../services/file-upload.service.ts | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/apps/red-ui/src/app/modules/upload-download/services/file-upload.service.ts b/apps/red-ui/src/app/modules/upload-download/services/file-upload.service.ts index 160a9a7ef..5cd985a0e 100644 --- a/apps/red-ui/src/app/modules/upload-download/services/file-upload.service.ts +++ b/apps/red-ui/src/app/modules/upload-download/services/file-upload.service.ts @@ -1,22 +1,22 @@ -import { ApplicationRef, Injectable, OnDestroy } from '@angular/core'; -import { FileUploadModel } from '../model/file-upload.model'; import { HttpErrorResponse, HttpEventType, HttpStatusCode } from '@angular/common/http'; -import { interval, Subject, Subscription } from 'rxjs'; -import { ConfigService } from '@services/config.service'; +import { ApplicationRef, Injectable, OnDestroy } from '@angular/core'; +import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker'; +import { TenantsService } from '@common-ui/tenants'; +import { ErrorMessageService, GenericService, Toaster } from '@iqser/common-ui'; +import { HeadersConfiguration } from '@iqser/common-ui/lib/utils'; import { TranslateService } from '@ngx-translate/core'; import { IFileUploadResult, OverwriteFileOption, OverwriteFileOptions } from '@red/domain'; -import { isAcceptedFileType, isCsv, isDocument, isZip } from '@utils/file-drop-utils'; -import { ErrorMessageService, GenericService, Toaster } from '@iqser/common-ui'; +import { ConfigService } from '@services/config.service'; import { FilesMapService } from '@services/files/files-map.service'; -import { switchMap, tap, throttleTime } from 'rxjs/operators'; import { FilesService } from '@services/files/files.service'; -import { UploadDownloadDialogService } from './upload-download-dialog.service'; -import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker'; -import { HeadersConfiguration } from '@iqser/common-ui/lib/utils'; import { LicenseService } from '@services/license.service'; -import { LicenseFeatures } from '../../admin/screens/license/utils/constants'; -import { TenantsService } from '@common-ui/tenants'; import { UserPreferenceService } from '@users/user-preference.service'; +import { isAcceptedFileType, isCsv, isZip } from '@utils/file-drop-utils'; +import { interval, Subject, Subscription } from 'rxjs'; +import { switchMap, tap, throttleTime } from 'rxjs/operators'; +import { LicenseFeatures } from '../../admin/screens/license/utils/constants'; +import { FileUploadModel } from '../model/file-upload.model'; +import { UploadDownloadDialogService } from './upload-download-dialog.service'; export interface ActiveUpload { subscription: Subscription; From b3dc8b04c87d7606b0665f3acec35735a5070bd3 Mon Sep 17 00:00:00 2001 From: Dan Percic Date: Fri, 1 Nov 2024 19:22:50 +0200 Subject: [PATCH 10/13] wip web socket annotations --- .../src/app/guards/if-logged-in.guard.ts | 3 +- .../src/app/modules/admin/admin.routes.ts | 8 +- .../rules-screen/rules-screen.component.ts | 26 +++---- .../file-preview-screen.component.ts | 19 +++-- .../file-preview/file-preview.routes.ts | 4 +- .../src/app/services/copilot.service.ts | 13 +++- apps/red-ui/src/app/services/stomp.service.ts | 74 +++++++++++++++++++ .../src/app/services/web-socket.service.ts | 66 +---------------- apps/red-ui/src/app/users/red-role.guard.ts | 4 +- apps/red-ui/src/app/users/user.service.ts | 16 ++-- libs/common-ui | 2 +- .../src/lib/trash/trash-dossier.model.ts | 6 +- .../src/lib/trash/trash-file.model.ts | 10 +-- tsconfig.json | 1 + 14 files changed, 131 insertions(+), 121 deletions(-) create mode 100644 apps/red-ui/src/app/services/stomp.service.ts diff --git a/apps/red-ui/src/app/guards/if-logged-in.guard.ts b/apps/red-ui/src/app/guards/if-logged-in.guard.ts index 52ac6bb1c..4d7ed1b64 100644 --- a/apps/red-ui/src/app/guards/if-logged-in.guard.ts +++ b/apps/red-ui/src/app/guards/if-logged-in.guard.ts @@ -40,8 +40,7 @@ export function ifLoggedIn(): AsyncGuard { logger.info('[KEYCLOAK] Keycloak init...'); await keycloakInitializer(tenant); - logger.info('[KEYCLOAK] Keycloak init done!'); - console.log({ tenant }); + logger.info('[KEYCLOAK] Keycloak init done for tenant: ', { tenant }); await tenantsService.selectTenant(tenant); await usersService.initialize(); await licenseService.loadLicenses(); diff --git a/apps/red-ui/src/app/modules/admin/admin.routes.ts b/apps/red-ui/src/app/modules/admin/admin.routes.ts index 233f15870..9d978cc6a 100644 --- a/apps/red-ui/src/app/modules/admin/admin.routes.ts +++ b/apps/red-ui/src/app/modules/admin/admin.routes.ts @@ -87,12 +87,8 @@ const dossierTemplateIdRoutes: IqserRoutes = [ multi: true, useFactory: () => { const service = inject(CopilotService); - return () => { - setTimeout(() => { - service.connect('/api/llm/llm-websocket'); - console.log('Copilot ready'); - }, 2000); - }; + console.log('Prepare copilot'); + return () => service.connectAsync('/api/llm/llm-websocket'); }, }, ], diff --git a/apps/red-ui/src/app/modules/admin/screens/rules/rules-screen/rules-screen.component.ts b/apps/red-ui/src/app/modules/admin/screens/rules/rules-screen/rules-screen.component.ts index 79ecfa33a..6ccd48108 100644 --- a/apps/red-ui/src/app/modules/admin/screens/rules/rules-screen/rules-screen.component.ts +++ b/apps/red-ui/src/app/modules/admin/screens/rules/rules-screen/rules-screen.component.ts @@ -1,4 +1,4 @@ -import { AsyncPipe, NgForOf, NgIf, NgTemplateOutlet } from '@angular/common'; +import { AsyncPipe, NgIf, NgTemplateOutlet } from '@angular/common'; import { ChangeDetectionStrategy, ChangeDetectorRef, Component, computed, inject, input, OnInit, signal } from '@angular/core'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { FormsModule } from '@angular/forms'; @@ -7,7 +7,6 @@ import { MatTooltip } from '@angular/material/tooltip'; import { InputWithActionComponent } from '@common-ui/inputs/input-with-action/input-with-action.component'; import { TenantsService } from '@common-ui/tenants'; import { getCurrentUser } from '@common-ui/users'; -import { NamePipe } from '@common-ui/users/name.pipe'; import { ComponentCanDeactivate } from '@guards/can-deactivate.guard'; import { CircleButtonComponent, IconButtonComponent, IconButtonTypes, LoadingService, Toaster } from '@iqser/common-ui'; import { Debounce, IqserTooltipPositions } from '@iqser/common-ui/lib/utils'; @@ -19,6 +18,7 @@ import { EditorThemeService } from '@services/editor-theme.service'; import { PermissionsService } from '@services/permissions.service'; import { DatePipe } from '@shared/pipes/date.pipe'; import { BehaviorSubject, firstValueFrom } from 'rxjs'; +import { filter, map } from 'rxjs/operators'; import { RulesService } from '../../../services/rules.service'; import { rulesScreenTranslations } from '../../../translations/rules-screen-translations'; import ICodeEditor = monaco.editor.ICodeEditor; @@ -57,8 +57,6 @@ const RULE_VALIDATION_TIMEOUT = 2000; CircleButtonComponent, DatePipe, InputWithActionComponent, - NamePipe, - NgForOf, MatTooltip, ], }) @@ -102,16 +100,18 @@ export default class RulesScreenComponent implements OnInit, ComponentCanDeactiv const username = this.#currentUser.id; const tenant = inject(TenantsService).activeTenantId; this.#copilotService - .listen('/user/' + username + '/queue/' + tenant + '/rules-copilot') - .pipe(takeUntilDestroyed()) + .listen<{ token?: string }>('/user/' + username + '/queue/' + tenant + '/rules-copilot') + .pipe( + takeUntilDestroyed(), + map(res => res?.token), + filter(Boolean), + ) .subscribe(response => { console.log('WS response: ' + response); + this.responses$.next([...this.responses$.value, { text: response, date: new Date().toISOString() }]); }); - this.#copilotService.publish({ - destination: '/app/rules-copilot', - body: JSON.stringify({ prompts: ['manageradmin'] }), - }); + this.#copilotService.send('manageradmin'); } set isLeavingPage(isLeaving: boolean) { @@ -136,11 +136,7 @@ export default class RulesScreenComponent implements OnInit, ComponentCanDeactiv add(question: string) { console.log(question); - this.responses$.next([...this.responses$.value, { text: question, date: new Date().toISOString() }]); - this.#copilotService.publish({ - destination: '/app/rules-copilot', - body: JSON.stringify({ prompts: [question] }), - }); + this.#copilotService.send(question); } async ngOnInit() { diff --git a/apps/red-ui/src/app/modules/file-preview/file-preview-screen.component.ts b/apps/red-ui/src/app/modules/file-preview/file-preview-screen.component.ts index c29f7fd53..f6faaf9fc 100644 --- a/apps/red-ui/src/app/modules/file-preview/file-preview-screen.component.ts +++ b/apps/red-ui/src/app/modules/file-preview/file-preview-screen.component.ts @@ -1,5 +1,5 @@ import { NgIf } from '@angular/common'; -import { ChangeDetectorRef, Component, effect, NgZone, OnDestroy, OnInit, TemplateRef, untracked, ViewChild } from '@angular/core'; +import { ChangeDetectorRef, Component, effect, inject, NgZone, OnDestroy, OnInit, TemplateRef, untracked, ViewChild } from '@angular/core'; import { ActivatedRouteSnapshot, NavigationExtras, Router } from '@angular/router'; import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker'; import { ComponentCanDeactivate } from '@guards/can-deactivate.guard'; @@ -90,7 +90,14 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni }) private readonly _filterTemplate: TemplateRef; #loadAllAnnotationsEnabled = false; - readonly #wsConnection$: Observable; + readonly #wsConnection$ = inject(WebSocketService) + .listen(WsTopics.ANALYSIS) + .pipe( + log('[WS] Analysis events'), + filter(event => event.analyseStatus === AnalyseStatuses.FINISHED), + switchMap(event => this._fileDataService.updateAnnotations(this.state.file(), event.analysisNumber)), + log('[WS] Annotations updated'), + ); #wsConnectionSub: Subscription; protected readonly isDocumine = getConfig().IS_DOCUMINE; readonly circleButtonTypes = CircleButtonTypes; @@ -135,7 +142,6 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni private readonly _dossierTemplatesService: DossierTemplatesService, private readonly _multiSelectService: MultiSelectService, private readonly _documentInfoService: DocumentInfoService, - private readonly _webSocketService: WebSocketService, ) { super(); effect(() => { @@ -151,13 +157,6 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni } }); - this.#wsConnection$ = this._webSocketService.listen(WsTopics.ANALYSIS).pipe( - log('[WS] Analysis events'), - filter(event => event.analyseStatus === AnalyseStatuses.FINISHED), - switchMap(event => this._fileDataService.updateAnnotations(this.state.file(), event.analysisNumber)), - log('[CONNNEECCCCCTIIONSSS] Annotations updated'), - ); - const file = this.state.file(); console.log(file); console.log(this._fileDataService.annotations()); diff --git a/apps/red-ui/src/app/modules/file-preview/file-preview.routes.ts b/apps/red-ui/src/app/modules/file-preview/file-preview.routes.ts index 95d6028c9..657544132 100644 --- a/apps/red-ui/src/app/modules/file-preview/file-preview.routes.ts +++ b/apps/red-ui/src/app/modules/file-preview/file-preview.routes.ts @@ -26,9 +26,7 @@ export default [ multi: true, useFactory: () => { const service = inject(WebSocketService); - return () => { - setTimeout(() => service.connect('/redaction-gateway-v1/websocket'), 2000); - }; + return () => service.connectAsync('/redaction-gateway-v1/websocket'); }, }, ], diff --git a/apps/red-ui/src/app/services/copilot.service.ts b/apps/red-ui/src/app/services/copilot.service.ts index 4b929105c..fdcf5d55f 100644 --- a/apps/red-ui/src/app/services/copilot.service.ts +++ b/apps/red-ui/src/app/services/copilot.service.ts @@ -1,11 +1,18 @@ import { Injectable } from '@angular/core'; -import { WebSocketService } from '@services/web-socket.service'; +import { StompService } from '@services/stomp.service'; @Injectable({ providedIn: 'root', }) -export class CopilotService extends WebSocketService { - get topicPrefix(): string { +export class CopilotService extends StompService { + override get topicPrefix(): string { return ''; } + + override send(message: string) { + this.publish({ + destination: '/app/rules-copilot', + body: JSON.stringify({ prompts: [message] }), + }); + } } diff --git a/apps/red-ui/src/app/services/stomp.service.ts b/apps/red-ui/src/app/services/stomp.service.ts new file mode 100644 index 000000000..a5b5b5d12 --- /dev/null +++ b/apps/red-ui/src/app/services/stomp.service.ts @@ -0,0 +1,74 @@ +import { inject, Injectable } from '@angular/core'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { KeycloakStatusService } from '@common-ui/tenants'; +import { log } from '@common-ui/utils'; +import { getConfig } from '@iqser/common-ui'; +import { IMessage, IWatchParams, RxStomp } from '@stomp/rx-stomp'; +import { StompHeaders } from '@stomp/stompjs'; + +import { NGXLogger } from 'ngx-logger'; +import { firstValueFrom, Observable } from 'rxjs'; +import { map, tap } from 'rxjs/operators'; + +@Injectable() +export class StompService extends RxStomp { + readonly #logger = inject(NGXLogger); + readonly #config = getConfig(); + readonly #keycloakStatusService = inject(KeycloakStatusService); + + constructor() { + super(); + this.connectionState$.pipe(log('[WS] Connection state'), takeUntilDestroyed()).subscribe(); + this.webSocketErrors$.pipe(log('[WS] Errors'), takeUntilDestroyed()).subscribe(); + this.stompErrors$.pipe(takeUntilDestroyed()).subscribe(frame => { + console.error(frame); + console.error('Broker reported error: ' + frame.headers['message']); + console.error('Additional details: ' + frame.body); + }); + this.#keycloakStatusService.token$.pipe(takeUntilDestroyed()).subscribe(token => { + this.#logger.info('[WS] Update connectHeaders'); + this.configure({ + connectHeaders: { Authorization: 'Bearer ' + token }, + }); + }); + } + + get topicPrefix() { + return ''; + } + + send(value: unknown) { + throw 'Not implemented'; + } + + override watch(opts: IWatchParams): Observable; + override watch(destination: string, headers?: StompHeaders): Observable; + override watch(opts: string | IWatchParams, headers?: StompHeaders): Observable { + if (typeof opts === 'string') { + return super.watch(this.topicPrefix + opts, headers); + } + + return super.watch(opts); + } + + listen(topic: string): Observable { + return this.watch(topic).pipe( + log('[WS] Listen to topic ' + topic), + tap(msg => console.log(msg.body)), + map(msg => JSON.parse(msg.body)), + ); + } + + connect(url: string) { + this.configure({ + debug: (msg: string) => this.#logger.debug(msg), + brokerURL: this.#config.API_URL + url, + }); + + this.activate(); + } + + connectAsync(url: string) { + return firstValueFrom(this.#keycloakStatusService.token$.pipe(map(() => this.connect(url)))); + } +} diff --git a/apps/red-ui/src/app/services/web-socket.service.ts b/apps/red-ui/src/app/services/web-socket.service.ts index 41b08684e..0b7c70f0d 100644 --- a/apps/red-ui/src/app/services/web-socket.service.ts +++ b/apps/red-ui/src/app/services/web-socket.service.ts @@ -1,72 +1,14 @@ -import { DestroyRef, inject, Injectable } from '@angular/core'; -import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { inject, Injectable } from '@angular/core'; import { TenantsService } from '@common-ui/tenants'; -import { log } from '@common-ui/utils'; -import { getConfig } from '@iqser/common-ui'; -import { IMessage, IWatchParams, RxStomp } from '@stomp/rx-stomp'; -import { StompHeaders } from '@stomp/stompjs'; - -import { NGXLogger } from 'ngx-logger'; -import { Observable } from 'rxjs'; -import { map, tap } from 'rxjs/operators'; +import { StompService } from '@services/stomp.service'; @Injectable({ providedIn: 'root', }) -export class WebSocketService extends RxStomp { - readonly #logger = inject(NGXLogger); - readonly #config = getConfig(); +export class WebSocketService extends StompService { readonly #tenantService = inject(TenantsService); - readonly #destroyRef = inject(DestroyRef); - constructor() { - super(); - } - - get topicPrefix() { + override get topicPrefix() { return '/topic/' + this.#tenantService.activeTenantId + '/'; } - - watch(opts: IWatchParams): Observable; - watch(destination: string, headers?: StompHeaders): Observable; - watch(opts: string | IWatchParams, headers?: StompHeaders): Observable { - if (typeof opts === 'string') { - return super.watch(this.topicPrefix + opts, headers); - } - - return super.watch(opts); - } - - listen(topic: string): Observable { - return this.watch(topic).pipe( - log('LISTEN LOG'), - tap(msg => console.log(msg.body)), - map(msg => JSON.parse(msg.body)), - ); - } - - connect(url: string) { - const headers = { Authorization: 'Bearer ' + localStorage.getItem('token') }; - console.log(headers); - this.configure({ - debug: (msg: string) => this.#logger.debug(msg), - brokerURL: this.#config.API_URL + url, - connectHeaders: headers, - }); - - this.connectionState$.pipe(log('[WS] Connection state')).subscribe(); - this.webSocketErrors$.pipe(log('[WS] Errors')).subscribe(); - this.stompErrors$ - .pipe( - tap(frame => { - console.error(frame); - console.error('Broker reported error: ' + frame.headers['message']); - console.error('Additional details: ' + frame.body); - }), - takeUntilDestroyed(this.#destroyRef), - ) - .subscribe(); - - this.activate(); - } } diff --git a/apps/red-ui/src/app/users/red-role.guard.ts b/apps/red-ui/src/app/users/red-role.guard.ts index ab0526c8e..d91daa5c5 100644 --- a/apps/red-ui/src/app/users/red-role.guard.ts +++ b/apps/red-ui/src/app/users/red-role.guard.ts @@ -2,7 +2,6 @@ import { inject, Injectable } from '@angular/core'; import { ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router'; import { IqserPermissionsService } from '@iqser/common-ui'; import { IqserRoleGuard } from '@iqser/common-ui/lib/users'; -import { UserService } from '@users/user.service'; import { NGXLogger } from 'ngx-logger'; @Injectable({ @@ -10,10 +9,9 @@ import { NGXLogger } from 'ngx-logger'; }) export class RedRoleGuard extends IqserRoleGuard { protected readonly _permissionsService = inject(IqserPermissionsService); - protected readonly _userService = inject(UserService); protected readonly _logger = inject(NGXLogger); - async canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Promise { + override async canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Promise { const currentUser = this._userService.currentUser; if (!currentUser?.hasAnyRole) { diff --git a/apps/red-ui/src/app/users/user.service.ts b/apps/red-ui/src/app/users/user.service.ts index aaf2a7074..5490603fd 100644 --- a/apps/red-ui/src/app/users/user.service.ts +++ b/apps/red-ui/src/app/users/user.service.ts @@ -1,10 +1,10 @@ import { inject, Injectable } from '@angular/core'; -import { User } from '@red/domain'; import { QueryParam } from '@iqser/common-ui'; -import { Roles } from '@users/roles'; -import { of } from 'rxjs'; import { IIqserUser, IqserUserService } from '@iqser/common-ui/lib/users'; import { List } from '@iqser/common-ui/lib/utils'; +import { User } from '@red/domain'; +import { Roles } from '@users/roles'; +import { of } from 'rxjs'; @Injectable({ providedIn: 'root', @@ -13,7 +13,7 @@ export class UserService extends IqserUserService { protected readonly _defaultModelPath = 'user'; protected readonly _entityClass = User; - async loadCurrentUser(): Promise { + override async loadCurrentUser(): Promise { const currentUser = await super.loadCurrentUser(); this._permissionsService.add({ @@ -23,12 +23,12 @@ export class UserService extends IqserUserService { return currentUser; } - loadAll() { + override loadAll() { const canReadUsers = this._permissionsService.has(Roles.users.read); return canReadUsers ? super.loadAll() : of([]); } - getAll() { + override getAll() { const canReadAllUsers = this._permissionsService.has(Roles.users.readAll); const url = canReadAllUsers ? this._defaultModelPath : `${this._defaultModelPath}/red`; return super.getAll(url); @@ -39,8 +39,8 @@ export class UserService extends IqserUserService { return this._post(null, `${this._defaultModelPath}/profile/activate/${user.userId}`, queryParams); } - protected readonly _rolesFilter = (role: string) => role.startsWith('RED_'); - protected readonly _permissionsFilter = (role: string) => role.startsWith('red-') || role.startsWith('fforesight-'); + protected override readonly _rolesFilter = (role: string) => role.startsWith('RED_'); + protected override readonly _permissionsFilter = (role: string) => role.startsWith('red-') || role.startsWith('fforesight-'); } export function getCurrentUser() { diff --git a/libs/common-ui b/libs/common-ui index 17d2e8c53..ebaf1709b 160000 --- a/libs/common-ui +++ b/libs/common-ui @@ -1 +1 @@ -Subproject commit 17d2e8c530700094d783ba912e9cb71c23621547 +Subproject commit ebaf1709b1138b6da5b329078362c6e34459e4a2 diff --git a/libs/red-domain/src/lib/trash/trash-dossier.model.ts b/libs/red-domain/src/lib/trash/trash-dossier.model.ts index c7a579f96..8213a3b45 100644 --- a/libs/red-domain/src/lib/trash/trash-dossier.model.ts +++ b/libs/red-domain/src/lib/trash/trash-dossier.model.ts @@ -19,9 +19,9 @@ export class TrashDossier extends TrashItem implements Partial { constructor( dossier: IDossier, - protected readonly _retentionHours: number, - readonly hasRestoreRights: boolean, - readonly hasHardDeleteRights: boolean, + protected override readonly _retentionHours: number, + override readonly hasRestoreRights: boolean, + override readonly hasHardDeleteRights: boolean, readonly ownerName: string, ) { super(_retentionHours, dossier.softDeletedTime || '-', hasRestoreRights, hasHardDeleteRights); diff --git a/libs/red-domain/src/lib/trash/trash-file.model.ts b/libs/red-domain/src/lib/trash/trash-file.model.ts index e12ea032f..e221ff205 100644 --- a/libs/red-domain/src/lib/trash/trash-file.model.ts +++ b/libs/red-domain/src/lib/trash/trash-file.model.ts @@ -1,6 +1,6 @@ -import { TrashItem } from './trash.item'; -import { File, IFile } from '../files'; import { FileAttributes } from '../file-attributes'; +import { File, IFile } from '../files'; +import { TrashItem } from './trash.item'; export class TrashFile extends TrashItem implements Partial { readonly type = 'file'; @@ -22,9 +22,9 @@ export class TrashFile extends TrashItem implements Partial { constructor( file: File, readonly dossierTemplateId: string, - protected readonly _retentionHours: number, - readonly hasRestoreRights: boolean, - readonly hasHardDeleteRights: boolean, + protected override readonly _retentionHours: number, + override readonly hasRestoreRights: boolean, + override readonly hasHardDeleteRights: boolean, readonly ownerName: string, readonly fileDossierName: string, ) { diff --git a/tsconfig.json b/tsconfig.json index e179555d9..c194700e5 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -12,6 +12,7 @@ "useDefineForClassFields": false, "strictPropertyInitialization": false, "importHelpers": true, + // "noImplicitOverride": true, "target": "ES2022", "module": "ES2022", "typeRoots": ["node_modules/@types"], From ff8009167b693f0256bea90c7d3f1e9effe124f1 Mon Sep 17 00:00:00 2001 From: Dan Percic Date: Mon, 4 Nov 2024 13:03:12 +0200 Subject: [PATCH 11/13] other fixes --- .../annotation-actions.component.html | 17 +++++++++-------- .../annotation-actions.component.ts | 4 ++-- .../file-preview-screen.component.ts | 2 -- apps/red-ui/src/app/services/stomp.service.ts | 15 +++++++-------- 4 files changed, 18 insertions(+), 20 deletions(-) diff --git a/apps/red-ui/src/app/modules/file-preview/components/annotation-actions/annotation-actions.component.html b/apps/red-ui/src/app/modules/file-preview/components/annotation-actions/annotation-actions.component.html index 3a79d9f8e..3d08ac883 100644 --- a/apps/red-ui/src/app/modules/file-preview/components/annotation-actions/annotation-actions.component.html +++ b/apps/red-ui/src/app/modules/file-preview/components/annotation-actions/annotation-actions.component.html @@ -83,14 +83,15 @@ icon="iqser:trash" > - + + + ; @@ -53,8 +52,7 @@ export class StompService extends RxStomp { listen(topic: string): Observable { return this.watch(topic).pipe( - log('[WS] Listen to topic ' + topic), - tap(msg => console.log(msg.body)), + tap(msg => this.#logger.info('[WS] Response on topic ' + topic, msg.body)), map(msg => JSON.parse(msg.body)), ); } @@ -63,12 +61,13 @@ export class StompService extends RxStomp { this.configure({ debug: (msg: string) => this.#logger.debug(msg), brokerURL: this.#config.API_URL + url, + reconnectDelay: 0, }); this.activate(); } - connectAsync(url: string) { - return firstValueFrom(this.#keycloakStatusService.token$.pipe(map(() => this.connect(url)))); + async connectAsync(url: string) { + return await firstValueFrom(this.#keycloakStatusService.token$.pipe(map(() => this.connect(url)))); } } From 306b524deec52d5cf95edb2fae356e6b08921fe0 Mon Sep 17 00:00:00 2001 From: Dan Percic Date: Tue, 5 Nov 2024 21:58:07 +0200 Subject: [PATCH 12/13] copilot working as a conversation --- .../rules-screen/rules-screen.component.html | 9 ++-- .../rules-screen/rules-screen.component.scss | 6 ++- .../rules-screen/rules-screen.component.ts | 53 +++++++++++++++---- 3 files changed, 50 insertions(+), 18 deletions(-) diff --git a/apps/red-ui/src/app/modules/admin/screens/rules/rules-screen/rules-screen.component.html b/apps/red-ui/src/app/modules/admin/screens/rules/rules-screen/rules-screen.component.html index f2b47dab2..66e408b39 100644 --- a/apps/red-ui/src/app/modules/admin/screens/rules/rules-screen/rules-screen.component.html +++ b/apps/red-ui/src/app/modules/admin/screens/rules/rules-screen/rules-screen.component.html @@ -12,23 +12,20 @@
-
Copilot
+
Copilot
- @for (comment of responses$ | async; track comment) { + @for (comment of conversation(); track comment) {
{{ comment.date | date: 'sophisticatedDate' }}
- - -
-
{{ comment.text }}
+
{{ comment.text }}
} diff --git a/apps/red-ui/src/app/modules/admin/screens/rules/rules-screen/rules-screen.component.scss b/apps/red-ui/src/app/modules/admin/screens/rules/rules-screen/rules-screen.component.scss index 702bc576e..b36407be4 100644 --- a/apps/red-ui/src/app/modules/admin/screens/rules/rules-screen/rules-screen.component.scss +++ b/apps/red-ui/src/app/modules/admin/screens/rules/rules-screen/rules-screen.component.scss @@ -84,7 +84,7 @@ ngx-monaco-editor { .right-container { display: flex; - width: 375px; + width: 750px; min-width: 375px; padding: 16px 24px 16px 24px; @@ -96,3 +96,7 @@ ngx-monaco-editor { width: 100%; } } + +.text-auto { + text-wrap: auto; +} diff --git a/apps/red-ui/src/app/modules/admin/screens/rules/rules-screen/rules-screen.component.ts b/apps/red-ui/src/app/modules/admin/screens/rules/rules-screen/rules-screen.component.ts index 6ccd48108..05080d64f 100644 --- a/apps/red-ui/src/app/modules/admin/screens/rules/rules-screen/rules-screen.component.ts +++ b/apps/red-ui/src/app/modules/admin/screens/rules/rules-screen/rules-screen.component.ts @@ -1,5 +1,5 @@ -import { AsyncPipe, NgIf, NgTemplateOutlet } from '@angular/common'; -import { ChangeDetectionStrategy, ChangeDetectorRef, Component, computed, inject, input, OnInit, signal } from '@angular/core'; +import { NgIf, NgTemplateOutlet } from '@angular/common'; +import { ChangeDetectionStrategy, ChangeDetectorRef, Component, computed, inject, input, OnInit, signal, viewChild } from '@angular/core'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { FormsModule } from '@angular/forms'; import { MatIcon } from '@angular/material/icon'; @@ -17,8 +17,8 @@ import { CopilotService } from '@services/copilot.service'; import { EditorThemeService } from '@services/editor-theme.service'; import { PermissionsService } from '@services/permissions.service'; import { DatePipe } from '@shared/pipes/date.pipe'; -import { BehaviorSubject, firstValueFrom } from 'rxjs'; -import { filter, map } from 'rxjs/operators'; +import { firstValueFrom } from 'rxjs'; +import { map } from 'rxjs/operators'; import { RulesService } from '../../../services/rules.service'; import { rulesScreenTranslations } from '../../../translations/rules-screen-translations'; import ICodeEditor = monaco.editor.ICodeEditor; @@ -38,6 +38,21 @@ interface UploadResponse { deprecatedWarnings: SyntaxError[]; } +export const SentenceTypes = { + question: 'question', + answer: 'answer', +} as const; + +export type SentenceType = keyof typeof SentenceTypes; + +interface Sentence { + text: string | null; + date: string; + type: SentenceType; +} + +const endingSentence: Sentence = { text: null, date: new Date().toISOString(), type: SentenceTypes.answer }; + const RULE_VALIDATION_TIMEOUT = 2000; @Component({ @@ -52,7 +67,6 @@ const RULE_VALIDATION_TIMEOUT = 2000; IconButtonComponent, NgIf, TranslateModule, - AsyncPipe, NgTemplateOutlet, CircleButtonComponent, DatePipe, @@ -68,11 +82,13 @@ export default class RulesScreenComponent implements OnInit, ComponentCanDeactiv #ruleValidationTimeout: number = null; readonly #copilotService = inject(CopilotService); readonly #currentUser = getCurrentUser(); + readonly #conversation = signal([endingSentence]); protected readonly collapsed = signal(true); protected readonly IqserTooltipPositions = IqserTooltipPositions; readonly dossierTemplateId = input.required(); readonly translations = rulesScreenTranslations; readonly iconButtonTypes = IconButtonTypes; + readonly inputWithAction = viewChild(InputWithActionComponent); readonly editorOptions: IStandaloneEditorConstructionOptions = { theme: 'vs', language: 'java', @@ -87,7 +103,7 @@ export default class RulesScreenComponent implements OnInit, ComponentCanDeactiv readonly type = input.required(); readonly numberOfErrors = computed(() => this.#errors().filter(e => !e.warning).length); readonly numberOfWarnings = computed(() => this.#errors().filter(e => e.warning).length); - readonly responses$ = new BehaviorSubject<{ text: string; date: string }[]>([]); + readonly conversation = computed(() => this.#conversation().filter(r => !!r.text)); constructor( readonly permissionsService: PermissionsService, @@ -104,11 +120,18 @@ export default class RulesScreenComponent implements OnInit, ComponentCanDeactiv .pipe( takeUntilDestroyed(), map(res => res?.token), - filter(Boolean), ) - .subscribe(response => { - console.log('WS response: ' + response); - this.responses$.next([...this.responses$.value, { text: response, date: new Date().toISOString() }]); + .subscribe(token => { + console.log('WS token: ' + token); + if (token === null) { + console.log(this.#conversation()); + this.#conversation.update(responses => [...responses, { ...endingSentence }]); + return; + } + this.#conversation.update(responses => { + const last = responses.pop(); + return [...responses, { ...last, text: (last.text ?? '') + token }]; + }); }); this.#copilotService.send('manageradmin'); @@ -135,7 +158,15 @@ export default class RulesScreenComponent implements OnInit, ComponentCanDeactiv } add(question: string) { - console.log(question); + this.#conversation.update(responses => { + const last = responses.pop(); + last.text = question; + last.type = SentenceTypes.question; + last.date = new Date().toISOString(); + return [...responses, last, { ...endingSentence }]; + }); + console.log(this.#conversation()); + this.inputWithAction().reset(); this.#copilotService.send(question); } From 4933c7a678ae2511938d7d9b7bc27f942f36be4c Mon Sep 17 00:00:00 2001 From: Dan Percic Date: Mon, 9 Dec 2024 14:37:02 +0200 Subject: [PATCH 13/13] RED-9582 finish copilot --- .../src/app/modules/admin/admin.routes.ts | 14 +- .../rules-screen/rules-screen.component.html | 14 +- .../rules-screen/rules-screen.component.ts | 35 +++-- .../file-preview-screen.component.ts | 6 - .../file-preview/file-preview.routes.ts | 13 +- apps/red-ui/src/app/services/stomp.service.ts | 2 +- apps/red-ui/src/assets/i18n/redact/de.json | 137 +++++++++--------- apps/red-ui/src/assets/i18n/redact/en.json | 3 + apps/red-ui/src/assets/i18n/scm/de.json | 3 + apps/red-ui/src/assets/i18n/scm/en.json | 3 + 10 files changed, 122 insertions(+), 108 deletions(-) diff --git a/apps/red-ui/src/app/modules/admin/admin.routes.ts b/apps/red-ui/src/app/modules/admin/admin.routes.ts index 9d978cc6a..127879694 100644 --- a/apps/red-ui/src/app/modules/admin/admin.routes.ts +++ b/apps/red-ui/src/app/modules/admin/admin.routes.ts @@ -1,4 +1,4 @@ -import { ENVIRONMENT_INITIALIZER, inject } from '@angular/core'; +import { inject, provideEnvironmentInitializer } from '@angular/core'; import { PendingChangesGuard } from '@guards/can-deactivate.guard'; import { templateExistsWhenEnteringAdmin } from '@guards/dossier-template-exists.guard'; import { DossierTemplatesGuard } from '@guards/dossier-templates.guard'; @@ -82,15 +82,9 @@ const dossierTemplateIdRoutes: IqserRoutes = [ }, providers: [ RulesService, - { - provide: ENVIRONMENT_INITIALIZER, - multi: true, - useFactory: () => { - const service = inject(CopilotService); - console.log('Prepare copilot'); - return () => service.connectAsync('/api/llm/llm-websocket'); - }, - }, + provideEnvironmentInitializer(() => { + return inject(CopilotService).connectAsync('/api/llm/llm-websocket'); + }), ], }, { diff --git a/apps/red-ui/src/app/modules/admin/screens/rules/rules-screen/rules-screen.component.html b/apps/red-ui/src/app/modules/admin/screens/rules/rules-screen/rules-screen.component.html index 66e408b39..83e6cc357 100644 --- a/apps/red-ui/src/app/modules/admin/screens/rules/rules-screen/rules-screen.component.html +++ b/apps/red-ui/src/app/modules/admin/screens/rules/rules-screen/rules-screen.component.html @@ -7,13 +7,17 @@
- -
+ +
-
Copilot
- +
{{ 'copilot.label' | translate | titlecase }}
+
@@ -42,7 +46,7 @@ this.#copilotService.deactivate()); this.#copilotService .listen<{ token?: string }>('/user/' + username + '/queue/' + tenant + '/rules-copilot') .pipe( @@ -121,9 +134,7 @@ export default class RulesScreenComponent implements OnInit, ComponentCanDeactiv map(res => res?.token), ) .subscribe(token => { - console.log('WS token: ' + token); if (token === null) { - console.log(this.#conversation()); this.#conversation.update(responses => [...responses, { ...endingSentence }]); return; } @@ -132,13 +143,11 @@ export default class RulesScreenComponent implements OnInit, ComponentCanDeactiv return [...responses, { ...last, text: (last.text ?? '') + token }]; }); }); - - this.#copilotService.send('manageradmin'); } set isLeavingPage(isLeaving: boolean) { this.isLeaving = isLeaving; - this._changeDetectorRef.detectChanges(); + this._changeDetectorRef.markForCheck(); } get changed(): boolean { @@ -156,6 +165,13 @@ export default class RulesScreenComponent implements OnInit, ComponentCanDeactiv this.#closeProblemsView(); } + toggleCollapse() { + this.collapsed.update(collapsed => !collapsed); + if (this.#conversation().length === 1) { + this.#copilotService.send('Hello!'); + } + } + add(question: string) { this.#conversation.update(responses => { const last = responses.pop(); @@ -164,7 +180,6 @@ export default class RulesScreenComponent implements OnInit, ComponentCanDeactiv last.date = new Date().toISOString(); return [...responses, last, { ...endingSentence }]; }); - console.log(this.#conversation()); this.inputWithAction().reset(); this.#copilotService.send(question); } @@ -194,7 +209,7 @@ export default class RulesScreenComponent implements OnInit, ComponentCanDeactiv } (window as any).monaco.editor.setTheme(this._editorThemeService.getTheme(true)); await this.#configureSyntaxHighlighting(); - this._changeDetectorRef.detectChanges(); + this._changeDetectorRef.markForCheck(); } @Debounce() @@ -218,7 +233,7 @@ export default class RulesScreenComponent implements OnInit, ComponentCanDeactiv this.currentLines = this.initialLines; this.#decorations = this.#codeEditor?.deltaDecorations(this.#decorations, []) || []; this.#removeErrorMarkers(); - this._changeDetectorRef.detectChanges(); + this._changeDetectorRef.markForCheck(); this._loadingService.stop(); } diff --git a/apps/red-ui/src/app/modules/file-preview/file-preview-screen.component.ts b/apps/red-ui/src/app/modules/file-preview/file-preview-screen.component.ts index 1fd0d80e6..33a23038f 100644 --- a/apps/red-ui/src/app/modules/file-preview/file-preview-screen.component.ts +++ b/apps/red-ui/src/app/modules/file-preview/file-preview-screen.component.ts @@ -163,12 +163,6 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni private readonly _componentLogService: ComponentLogService, ) { super(); - effect(() => { - const file = this.state.file(); - console.log('FILE CHANGED'); - // this._fileDataService.loadAnnotations(file).then(); - }); - effect(() => { const file = this.state.file(); if (file.analysisRequired && !file.excludedFromAutomaticAnalysis) { diff --git a/apps/red-ui/src/app/modules/file-preview/file-preview.routes.ts b/apps/red-ui/src/app/modules/file-preview/file-preview.routes.ts index 657544132..86058303e 100644 --- a/apps/red-ui/src/app/modules/file-preview/file-preview.routes.ts +++ b/apps/red-ui/src/app/modules/file-preview/file-preview.routes.ts @@ -1,4 +1,4 @@ -import { ENVIRONMENT_INITIALIZER, inject } from '@angular/core'; +import { inject, provideEnvironmentInitializer } from '@angular/core'; import { PendingChangesGuard } from '@guards/can-deactivate.guard'; import { IqserRoutes } from '@iqser/common-ui'; import { WebSocketService } from '@services/web-socket.service'; @@ -21,14 +21,9 @@ export default [ DocumentUnloadedGuard, TablesService, FileAssignService, - { - provide: ENVIRONMENT_INITIALIZER, - multi: true, - useFactory: () => { - const service = inject(WebSocketService); - return () => service.connectAsync('/redaction-gateway-v1/websocket'); - }, - }, + provideEnvironmentInitializer(async () => { + return inject(WebSocketService).connectAsync('/redaction-gateway-v1/websocket'); + }), ], }, ] satisfies IqserRoutes; diff --git a/apps/red-ui/src/app/services/stomp.service.ts b/apps/red-ui/src/app/services/stomp.service.ts index 9b132c76e..c6f1bfddc 100644 --- a/apps/red-ui/src/app/services/stomp.service.ts +++ b/apps/red-ui/src/app/services/stomp.service.ts @@ -59,7 +59,7 @@ export abstract class StompService extends RxStomp { connect(url: string) { this.configure({ - debug: (msg: string) => this.#logger.debug(msg), + debug: (msg: string) => this.#logger.debug('[WS] ' + msg), brokerURL: this.#config.API_URL + url, reconnectDelay: 0, }); diff --git a/apps/red-ui/src/assets/i18n/redact/de.json b/apps/red-ui/src/assets/i18n/redact/de.json index c966525a6..dabfc0160 100644 --- a/apps/red-ui/src/assets/i18n/redact/de.json +++ b/apps/red-ui/src/assets/i18n/redact/de.json @@ -275,9 +275,6 @@ "watermarks": "Wasserzeichen" }, "analysis-disabled": "", - "annotation": { - "pending": "(Analyse steht aus)" - }, "annotation-actions": { "accept-recommendation": { "label": "Empfehlung annehmen" @@ -333,14 +330,14 @@ "error": "Rekategorisierung des Bilds fehlgeschlagen: {error}", "success": "Bild wurde einer neuen Kategorie zugeordnet." }, - "remove": { - "error": "Entfernen der Schwärzung fehlgeschlagen: {error}", - "success": "Schwärzung wurde entfernt" - }, "remove-hint": { "error": "Entfernen des Hinweises fehlgeschlagen: {error}", "success": "Hinweis wurde entfernt" }, + "remove": { + "error": "Entfernen der Schwärzung fehlgeschlagen: {error}", + "success": "Schwärzung wurde entfernt" + }, "undo": { "error": "Die Aktion konnte nicht rückgängig gemacht werden. Fehler: {error}", "success": "Rücksetzung erfolgreich" @@ -353,15 +350,15 @@ "remove-highlights": { "label": "Ausgewählte Markierungen entfernen" }, - "resize": { - "label": "Größe ändern" - }, "resize-accept": { "label": "Neue Größe speichern" }, "resize-cancel": { "label": "Größenänderung abbrechen" }, + "resize": { + "label": "Größe ändern" + }, "see-references": { "label": "Referenzen anzeigen" }, @@ -396,6 +393,9 @@ "skipped": "Ignorierte Schwärzung", "text-highlight": "Markierung" }, + "annotation": { + "pending": "(Analyse steht aus)" + }, "annotations": "Annotationen", "archived-dossiers-listing": { "no-data": { @@ -702,6 +702,9 @@ } }, "content": "Grund", + "copilot": { + "label": "" + }, "dashboard": { "empty-template": { "description": "Diese Vorlage enthält keine Dossiers. Erstellen Sie ein Dossier, um nach diesen Regeln zu schwärzen.", @@ -1023,13 +1026,13 @@ "recent": "Neu ({hours} h)", "unassigned": "Keinem Bearbeiter zugewiesen" }, - "reanalyse": { - "action": "Datei analysieren" - }, "reanalyse-dossier": { "error": "Einplanung der Dateien für die Reanalyse fehlgeschlagen. Bitte versuchen Sie es noch einmal.", "success": "Dateien für Reanalyse vorgesehen." }, + "reanalyse": { + "action": "Datei analysieren" + }, "report-download": "", "start-auto-analysis": "Auto-Analyse aktivieren", "stop-auto-analysis": "Auto-Analyse anhalten", @@ -1105,14 +1108,6 @@ "total-documents": "Dokumente", "total-people": "{count} {count, plural, one{Benutzer} other {Benutzer}}" }, - "dossier-templates": { - "label": "Dossier-Vorlagen", - "status": { - "active": "Aktiv", - "inactive": "Inaktiv", - "incomplete": "Unvollständig" - } - }, "dossier-templates-listing": { "action": { "clone": "Vorlage klonen", @@ -1147,6 +1142,14 @@ "title": "{length} {length, plural, one{Dossier-Vorlage} other{Dossier-Vorlagen}}" } }, + "dossier-templates": { + "label": "Dossier-Vorlagen", + "status": { + "active": "Aktiv", + "inactive": "Inaktiv", + "incomplete": "Unvollständig" + } + }, "dossier-watermark-selector": { "heading": "Wasserzeichen auf Dokumenten", "no-watermark": "Kein Wasserzeichen in der Dossier-Vorlage verfügbar:
Bitten Sie Ihren Admin, eines zu konfigurieren.", @@ -1357,15 +1360,6 @@ "title": "{length} {length, plural, one{Wörterbuch} other{Wörterbücher}}" } }, - "entity": { - "info": { - "actions": { - "revert": "Zurücksetzen", - "save": "Änderungen speichern" - }, - "heading": "Entität bearbeiten" - } - }, "entity-rules-screen": { "error": { "generic": "Fehler: Aktualisierung der Entitätsregeln fehlgeschlagen." @@ -1379,19 +1373,28 @@ "title": "Entitätsregeln-Editor", "warnings-found": "{warnings, plural, one{A warning} other{{warnings} warnings}} in Regeln gefunden" }, + "entity": { + "info": { + "actions": { + "revert": "Zurücksetzen", + "save": "Änderungen speichern" + }, + "heading": "Entität bearbeiten" + } + }, "error": { "deleted-entity": { "dossier": { "action": "Zurück zur Übersicht", "label": "Dieses Dossier wurde gelöscht!" }, - "file": { - "action": "Zurück zum Dossier", - "label": "Diese Datei wurde gelöscht!" - }, "file-dossier": { "action": "Zurück zur Übersicht", "label": "Das Dossier dieser Datei wurde gelöscht!" + }, + "file": { + "action": "Zurück zum Dossier", + "label": "Diese Datei wurde gelöscht!" } }, "file-preview": { @@ -1409,12 +1412,6 @@ }, "exact-date": "{day}. {month} {year} um {hour}:{minute} Uhr", "file": "Datei", - "file-attribute": { - "update": { - "error": "Aktualisierung des Werts für das Datei-Attribut fehlgeschlagen. Bitte versuchen Sie es noch einmal.", - "success": "Der Wert für das Dateiattribut wurde erfolgreich aktualisiert." - } - }, "file-attribute-encoding-types": { "ascii": "ASCII", "iso": "ISO-8859-1", @@ -1425,6 +1422,12 @@ "number": "Nummer", "text": "Freier Text" }, + "file-attribute": { + "update": { + "error": "Aktualisierung des Werts für das Datei-Attribut fehlgeschlagen. Bitte versuchen Sie es noch einmal.", + "success": "Der Wert für das Dateiattribut wurde erfolgreich aktualisiert." + } + }, "file-attributes-configurations": { "cancel": "Abbrechen", "form": { @@ -1642,15 +1645,6 @@ "zip": "Die Zip-Datei wurde erfolgreich hochgeladen!" } }, - "filter": { - "analysis": "Analyse erforderlich", - "comment": "Kommentare", - "hint": "Nur Hinweise", - "image": "Bilder", - "none": "Keine Annotationen", - "redaction": "Schwärzung", - "updated": "Aktualisiert" - }, "filter-menu": { "filter-options": "Filteroptionen", "filter-types": "Filter", @@ -1660,6 +1654,15 @@ "unseen-pages": "Nur Annotationen auf ungesehenen Seiten", "with-comments": "Nur Annotationen mit Kommentaren" }, + "filter": { + "analysis": "Analyse erforderlich", + "comment": "Kommentare", + "hint": "Nur Hinweise", + "image": "Bilder", + "none": "Keine Annotationen", + "redaction": "Schwärzung", + "updated": "Aktualisiert" + }, "filters": { "assigned-people": "Bearbeiter", "documents-status": "Dokumentenstatus", @@ -1949,13 +1952,6 @@ "user-promoted-to-approver": "Sie wurden zum Genehmiger in einem Dossier ernannt: {dossierHref, select, null{{dossierName}} other{{dossierName}}}", "user-removed-as-dossier-member": "Sie wurden als Dossier-Mitglied entfernt: \n{dossierHref, select, null{{dossierName}} other\n{{dossierName}}}\n" }, - "notifications": { - "button-text": "Benachrichtigungen", - "deleted-dossier": "Gelöschtes Dossier", - "label": "Benachrichtigungen", - "mark-all-as-read": "Alle als gelesen markieren", - "mark-as": "Als {type, select, read{gelesen} unread{ungelesen} other{}} markieren" - }, "notifications-screen": { "category": { "email-notifications": "E-Mail-Benachrichtigungen", @@ -1969,6 +1965,7 @@ "dossier": "Benachrichtigungen zu Dossiers", "other": "Andere Benachrichtigungen" }, + "options-title": "Wählen Sie aus, bei welchen Aktivitäten Sie benachrichtigt werden möchten", "options": { "ASSIGN_APPROVER": "Wenn ich einem Dokument als Genehmiger zugewiesen werde", "ASSIGN_REVIEWER": "Wenn ich einem Dokument als Prüfer zugewiesen werde", @@ -1986,7 +1983,6 @@ "USER_PROMOTED_TO_APPROVER": "Wenn ich Genehmiger in einem Dossier werde", "USER_REMOVED_AS_DOSSIER_MEMBER": "Wenn ich die Dossier-Mitgliedschaft verliere" }, - "options-title": "Wählen Sie aus, bei welchen Aktivitäten Sie benachrichtigt werden möchten", "schedule": { "daily": "Tägliche Zusammenfassung", "instant": "Sofort", @@ -1994,6 +1990,13 @@ }, "title": "Benachrichtigungseinstellungen" }, + "notifications": { + "button-text": "Benachrichtigungen", + "deleted-dossier": "Gelöschtes Dossier", + "label": "Benachrichtigungen", + "mark-all-as-read": "Alle als gelesen markieren", + "mark-as": "Als {type, select, read{gelesen} unread{ungelesen} other{}} markieren" + }, "ocr": { "confirmation-dialog": { "cancel": "Abbrechen", @@ -2105,10 +2108,6 @@ "warnings-label": "Dialoge und Meldungen", "warnings-subtitle": "„Nicht mehr anzeigen“-Optionen" }, - "processing": { - "basic": "Verarbeitung läuft", - "ocr": "OCR" - }, "processing-status": { "ocr": "OCR", "pending": "Ausstehend", @@ -2116,6 +2115,10 @@ "processed": "Verarbeitet", "processing": "Verarbeitung läuft" }, + "processing": { + "basic": "Verarbeitung läuft", + "ocr": "OCR" + }, "readonly": "Lesemodus", "readonly-archived": "Lesemodus (archiviert)", "redact-text": { @@ -2385,12 +2388,6 @@ "red-user-admin": "{count, plural, one{Benutzeradmin} other{Benutzeradmins}}", "regular": "{count, plural, one{regulärer Benutzer} other{reguläre Benutzer}}" }, - "search": { - "active-dossiers": "Dokumente in aktiven Dossiers", - "all-dossiers": "Alle Dokumente", - "placeholder": "Dokumente durchsuchen...", - "this-dossier": "In diesem Dossier" - }, "search-screen": { "cols": { "assignee": "Bearbeiter", @@ -2414,6 +2411,12 @@ "no-match": "Der Suchbegriff wurde in keinem der Dokumente gefunden.", "table-header": "{length} {length, plural, one{Suchergebnis} other{Suchergebnisse}}" }, + "search": { + "active-dossiers": "Dokumente in aktiven Dossiers", + "all-dossiers": "Alle Dokumente", + "placeholder": "Dokumente durchsuchen...", + "this-dossier": "In diesem Dossier" + }, "seconds": "Sekunden", "size": "Größe", "smtp-auth-config": { diff --git a/apps/red-ui/src/assets/i18n/redact/en.json b/apps/red-ui/src/assets/i18n/redact/en.json index 09d91a039..90519f397 100644 --- a/apps/red-ui/src/assets/i18n/redact/en.json +++ b/apps/red-ui/src/assets/i18n/redact/en.json @@ -702,6 +702,9 @@ } }, "content": "Reason", + "copilot": { + "label": "Copilot" + }, "dashboard": { "empty-template": { "description": "This template does not contain any dossiers. Create a dossier that applies this ruleset.", diff --git a/apps/red-ui/src/assets/i18n/scm/de.json b/apps/red-ui/src/assets/i18n/scm/de.json index 475b7e885..350f97a69 100644 --- a/apps/red-ui/src/assets/i18n/scm/de.json +++ b/apps/red-ui/src/assets/i18n/scm/de.json @@ -702,6 +702,9 @@ } }, "content": "Grund", + "copilot": { + "label": "" + }, "dashboard": { "empty-template": { "description": "Diese Vorlage enthält keine Dossiers. Erstellen Sie ein Dossier, um nach diesen Regeln zu schwärzen.", diff --git a/apps/red-ui/src/assets/i18n/scm/en.json b/apps/red-ui/src/assets/i18n/scm/en.json index 0d8ad1628..93aafcfb6 100644 --- a/apps/red-ui/src/assets/i18n/scm/en.json +++ b/apps/red-ui/src/assets/i18n/scm/en.json @@ -702,6 +702,9 @@ } }, "content": "Reason", + "copilot": { + "label": "Copilot" + }, "dashboard": { "empty-template": { "description": "This template does not contain any dossiers. Create a dossier that applies this ruleset.",