wip web socket annotations

This commit is contained in:
Dan Percic 2024-11-01 19:22:50 +02:00
parent c677bc17e4
commit b3dc8b04c8
14 changed files with 131 additions and 121 deletions

View File

@ -40,8 +40,7 @@ export function ifLoggedIn(): AsyncGuard {
logger.info('[KEYCLOAK] Keycloak init...'); logger.info('[KEYCLOAK] Keycloak init...');
await keycloakInitializer(tenant); await keycloakInitializer(tenant);
logger.info('[KEYCLOAK] Keycloak init done!'); logger.info('[KEYCLOAK] Keycloak init done for tenant: ', { tenant });
console.log({ tenant });
await tenantsService.selectTenant(tenant); await tenantsService.selectTenant(tenant);
await usersService.initialize(); await usersService.initialize();
await licenseService.loadLicenses(); await licenseService.loadLicenses();

View File

@ -87,12 +87,8 @@ const dossierTemplateIdRoutes: IqserRoutes = [
multi: true, multi: true,
useFactory: () => { useFactory: () => {
const service = inject(CopilotService); const service = inject(CopilotService);
return () => { console.log('Prepare copilot');
setTimeout(() => { return () => service.connectAsync('/api/llm/llm-websocket');
service.connect('/api/llm/llm-websocket');
console.log('Copilot ready');
}, 2000);
};
}, },
}, },
], ],

View File

@ -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 { ChangeDetectionStrategy, ChangeDetectorRef, Component, computed, inject, input, OnInit, signal } from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { FormsModule } from '@angular/forms'; 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 { InputWithActionComponent } from '@common-ui/inputs/input-with-action/input-with-action.component';
import { TenantsService } from '@common-ui/tenants'; import { TenantsService } from '@common-ui/tenants';
import { getCurrentUser } from '@common-ui/users'; import { getCurrentUser } from '@common-ui/users';
import { NamePipe } from '@common-ui/users/name.pipe';
import { ComponentCanDeactivate } from '@guards/can-deactivate.guard'; import { ComponentCanDeactivate } from '@guards/can-deactivate.guard';
import { CircleButtonComponent, IconButtonComponent, IconButtonTypes, LoadingService, Toaster } from '@iqser/common-ui'; import { CircleButtonComponent, IconButtonComponent, IconButtonTypes, LoadingService, Toaster } from '@iqser/common-ui';
import { Debounce, IqserTooltipPositions } from '@iqser/common-ui/lib/utils'; 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 { PermissionsService } from '@services/permissions.service';
import { DatePipe } from '@shared/pipes/date.pipe'; import { DatePipe } from '@shared/pipes/date.pipe';
import { BehaviorSubject, firstValueFrom } from 'rxjs'; import { BehaviorSubject, firstValueFrom } from 'rxjs';
import { filter, map } from 'rxjs/operators';
import { RulesService } from '../../../services/rules.service'; import { RulesService } from '../../../services/rules.service';
import { rulesScreenTranslations } from '../../../translations/rules-screen-translations'; import { rulesScreenTranslations } from '../../../translations/rules-screen-translations';
import ICodeEditor = monaco.editor.ICodeEditor; import ICodeEditor = monaco.editor.ICodeEditor;
@ -57,8 +57,6 @@ const RULE_VALIDATION_TIMEOUT = 2000;
CircleButtonComponent, CircleButtonComponent,
DatePipe, DatePipe,
InputWithActionComponent, InputWithActionComponent,
NamePipe,
NgForOf,
MatTooltip, MatTooltip,
], ],
}) })
@ -102,16 +100,18 @@ export default class RulesScreenComponent implements OnInit, ComponentCanDeactiv
const username = this.#currentUser.id; const username = this.#currentUser.id;
const tenant = inject(TenantsService).activeTenantId; const tenant = inject(TenantsService).activeTenantId;
this.#copilotService this.#copilotService
.listen('/user/' + username + '/queue/' + tenant + '/rules-copilot') .listen<{ token?: string }>('/user/' + username + '/queue/' + tenant + '/rules-copilot')
.pipe(takeUntilDestroyed()) .pipe(
takeUntilDestroyed(),
map(res => res?.token),
filter(Boolean),
)
.subscribe(response => { .subscribe(response => {
console.log('WS response: ' + response); console.log('WS response: ' + response);
this.responses$.next([...this.responses$.value, { text: response, date: new Date().toISOString() }]);
}); });
this.#copilotService.publish({ this.#copilotService.send('manageradmin');
destination: '/app/rules-copilot',
body: JSON.stringify({ prompts: ['manageradmin'] }),
});
} }
set isLeavingPage(isLeaving: boolean) { set isLeavingPage(isLeaving: boolean) {
@ -136,11 +136,7 @@ export default class RulesScreenComponent implements OnInit, ComponentCanDeactiv
add(question: string) { add(question: string) {
console.log(question); console.log(question);
this.responses$.next([...this.responses$.value, { text: question, date: new Date().toISOString() }]); this.#copilotService.send(question);
this.#copilotService.publish({
destination: '/app/rules-copilot',
body: JSON.stringify({ prompts: [question] }),
});
} }
async ngOnInit() { async ngOnInit() {

View File

@ -1,5 +1,5 @@
import { NgIf } from '@angular/common'; 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 { ActivatedRouteSnapshot, NavigationExtras, Router } from '@angular/router';
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker'; import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
import { ComponentCanDeactivate } from '@guards/can-deactivate.guard'; import { ComponentCanDeactivate } from '@guards/can-deactivate.guard';
@ -90,7 +90,14 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni
}) })
private readonly _filterTemplate: TemplateRef<unknown>; private readonly _filterTemplate: TemplateRef<unknown>;
#loadAllAnnotationsEnabled = false; #loadAllAnnotationsEnabled = false;
readonly #wsConnection$: Observable<unknown>; readonly #wsConnection$ = inject(WebSocketService)
.listen<AnalysisEvent>(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; #wsConnectionSub: Subscription;
protected readonly isDocumine = getConfig().IS_DOCUMINE; protected readonly isDocumine = getConfig().IS_DOCUMINE;
readonly circleButtonTypes = CircleButtonTypes; readonly circleButtonTypes = CircleButtonTypes;
@ -135,7 +142,6 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni
private readonly _dossierTemplatesService: DossierTemplatesService, private readonly _dossierTemplatesService: DossierTemplatesService,
private readonly _multiSelectService: MultiSelectService, private readonly _multiSelectService: MultiSelectService,
private readonly _documentInfoService: DocumentInfoService, private readonly _documentInfoService: DocumentInfoService,
private readonly _webSocketService: WebSocketService,
) { ) {
super(); super();
effect(() => { effect(() => {
@ -151,13 +157,6 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni
} }
}); });
this.#wsConnection$ = this._webSocketService.listen<AnalysisEvent>(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(); const file = this.state.file();
console.log(file); console.log(file);
console.log(this._fileDataService.annotations()); console.log(this._fileDataService.annotations());

View File

@ -26,9 +26,7 @@ export default [
multi: true, multi: true,
useFactory: () => { useFactory: () => {
const service = inject(WebSocketService); const service = inject(WebSocketService);
return () => { return () => service.connectAsync('/redaction-gateway-v1/websocket');
setTimeout(() => service.connect('/redaction-gateway-v1/websocket'), 2000);
};
}, },
}, },
], ],

View File

@ -1,11 +1,18 @@
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { WebSocketService } from '@services/web-socket.service'; import { StompService } from '@services/stomp.service';
@Injectable({ @Injectable({
providedIn: 'root', providedIn: 'root',
}) })
export class CopilotService extends WebSocketService { export class CopilotService extends StompService {
get topicPrefix(): string { override get topicPrefix(): string {
return ''; return '';
} }
override send(message: string) {
this.publish({
destination: '/app/rules-copilot',
body: JSON.stringify({ prompts: [message] }),
});
}
} }

View File

@ -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<IMessage>;
override watch(destination: string, headers?: StompHeaders): Observable<IMessage>;
override watch(opts: string | IWatchParams, headers?: StompHeaders): Observable<IMessage> {
if (typeof opts === 'string') {
return super.watch(this.topicPrefix + opts, headers);
}
return super.watch(opts);
}
listen<T>(topic: string): Observable<T> {
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))));
}
}

View File

@ -1,72 +1,14 @@
import { DestroyRef, inject, Injectable } from '@angular/core'; import { inject, Injectable } from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { TenantsService } from '@common-ui/tenants'; import { TenantsService } from '@common-ui/tenants';
import { log } from '@common-ui/utils'; import { StompService } from '@services/stomp.service';
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({ @Injectable({
providedIn: 'root', providedIn: 'root',
}) })
export class WebSocketService extends RxStomp { export class WebSocketService extends StompService {
readonly #logger = inject(NGXLogger);
readonly #config = getConfig();
readonly #tenantService = inject(TenantsService); readonly #tenantService = inject(TenantsService);
readonly #destroyRef = inject(DestroyRef);
constructor() { override get topicPrefix() {
super();
}
get topicPrefix() {
return '/topic/' + this.#tenantService.activeTenantId + '/'; return '/topic/' + this.#tenantService.activeTenantId + '/';
} }
watch(opts: IWatchParams): Observable<IMessage>;
watch(destination: string, headers?: StompHeaders): Observable<IMessage>;
watch(opts: string | IWatchParams, headers?: StompHeaders): Observable<IMessage> {
if (typeof opts === 'string') {
return super.watch(this.topicPrefix + opts, headers);
}
return super.watch(opts);
}
listen<T>(topic: string): Observable<T> {
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();
}
} }

View File

@ -2,7 +2,6 @@ import { inject, Injectable } from '@angular/core';
import { ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router'; import { ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router';
import { IqserPermissionsService } from '@iqser/common-ui'; import { IqserPermissionsService } from '@iqser/common-ui';
import { IqserRoleGuard } from '@iqser/common-ui/lib/users'; import { IqserRoleGuard } from '@iqser/common-ui/lib/users';
import { UserService } from '@users/user.service';
import { NGXLogger } from 'ngx-logger'; import { NGXLogger } from 'ngx-logger';
@Injectable({ @Injectable({
@ -10,10 +9,9 @@ import { NGXLogger } from 'ngx-logger';
}) })
export class RedRoleGuard extends IqserRoleGuard { export class RedRoleGuard extends IqserRoleGuard {
protected readonly _permissionsService = inject(IqserPermissionsService); protected readonly _permissionsService = inject(IqserPermissionsService);
protected readonly _userService = inject(UserService);
protected readonly _logger = inject(NGXLogger); protected readonly _logger = inject(NGXLogger);
async canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Promise<boolean> { override async canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Promise<boolean> {
const currentUser = this._userService.currentUser; const currentUser = this._userService.currentUser;
if (!currentUser?.hasAnyRole) { if (!currentUser?.hasAnyRole) {

View File

@ -1,10 +1,10 @@
import { inject, Injectable } from '@angular/core'; import { inject, Injectable } from '@angular/core';
import { User } from '@red/domain';
import { QueryParam } from '@iqser/common-ui'; 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 { IIqserUser, IqserUserService } from '@iqser/common-ui/lib/users';
import { List } from '@iqser/common-ui/lib/utils'; import { List } from '@iqser/common-ui/lib/utils';
import { User } from '@red/domain';
import { Roles } from '@users/roles';
import { of } from 'rxjs';
@Injectable({ @Injectable({
providedIn: 'root', providedIn: 'root',
@ -13,7 +13,7 @@ export class UserService extends IqserUserService<IIqserUser, User> {
protected readonly _defaultModelPath = 'user'; protected readonly _defaultModelPath = 'user';
protected readonly _entityClass = User; protected readonly _entityClass = User;
async loadCurrentUser(): Promise<User | undefined> { override async loadCurrentUser(): Promise<User | undefined> {
const currentUser = await super.loadCurrentUser(); const currentUser = await super.loadCurrentUser();
this._permissionsService.add({ this._permissionsService.add({
@ -23,12 +23,12 @@ export class UserService extends IqserUserService<IIqserUser, User> {
return currentUser; return currentUser;
} }
loadAll() { override loadAll() {
const canReadUsers = this._permissionsService.has(Roles.users.read); const canReadUsers = this._permissionsService.has(Roles.users.read);
return canReadUsers ? super.loadAll() : of([]); return canReadUsers ? super.loadAll() : of([]);
} }
getAll() { override getAll() {
const canReadAllUsers = this._permissionsService.has(Roles.users.readAll); const canReadAllUsers = this._permissionsService.has(Roles.users.readAll);
const url = canReadAllUsers ? this._defaultModelPath : `${this._defaultModelPath}/red`; const url = canReadAllUsers ? this._defaultModelPath : `${this._defaultModelPath}/red`;
return super.getAll(url); return super.getAll(url);
@ -39,8 +39,8 @@ export class UserService extends IqserUserService<IIqserUser, User> {
return this._post(null, `${this._defaultModelPath}/profile/activate/${user.userId}`, queryParams); return this._post(null, `${this._defaultModelPath}/profile/activate/${user.userId}`, queryParams);
} }
protected readonly _rolesFilter = (role: string) => role.startsWith('RED_'); protected override readonly _rolesFilter = (role: string) => role.startsWith('RED_');
protected readonly _permissionsFilter = (role: string) => role.startsWith('red-') || role.startsWith('fforesight-'); protected override readonly _permissionsFilter = (role: string) => role.startsWith('red-') || role.startsWith('fforesight-');
} }
export function getCurrentUser() { export function getCurrentUser() {

@ -1 +1 @@
Subproject commit 17d2e8c530700094d783ba912e9cb71c23621547 Subproject commit ebaf1709b1138b6da5b329078362c6e34459e4a2

View File

@ -19,9 +19,9 @@ export class TrashDossier extends TrashItem implements Partial<IDossier> {
constructor( constructor(
dossier: IDossier, dossier: IDossier,
protected readonly _retentionHours: number, protected override readonly _retentionHours: number,
readonly hasRestoreRights: boolean, override readonly hasRestoreRights: boolean,
readonly hasHardDeleteRights: boolean, override readonly hasHardDeleteRights: boolean,
readonly ownerName: string, readonly ownerName: string,
) { ) {
super(_retentionHours, dossier.softDeletedTime || '-', hasRestoreRights, hasHardDeleteRights); super(_retentionHours, dossier.softDeletedTime || '-', hasRestoreRights, hasHardDeleteRights);

View File

@ -1,6 +1,6 @@
import { TrashItem } from './trash.item';
import { File, IFile } from '../files';
import { FileAttributes } from '../file-attributes'; import { FileAttributes } from '../file-attributes';
import { File, IFile } from '../files';
import { TrashItem } from './trash.item';
export class TrashFile extends TrashItem implements Partial<IFile> { export class TrashFile extends TrashItem implements Partial<IFile> {
readonly type = 'file'; readonly type = 'file';
@ -22,9 +22,9 @@ export class TrashFile extends TrashItem implements Partial<IFile> {
constructor( constructor(
file: File, file: File,
readonly dossierTemplateId: string, readonly dossierTemplateId: string,
protected readonly _retentionHours: number, protected override readonly _retentionHours: number,
readonly hasRestoreRights: boolean, override readonly hasRestoreRights: boolean,
readonly hasHardDeleteRights: boolean, override readonly hasHardDeleteRights: boolean,
readonly ownerName: string, readonly ownerName: string,
readonly fileDossierName: string, readonly fileDossierName: string,
) { ) {

View File

@ -12,6 +12,7 @@
"useDefineForClassFields": false, "useDefineForClassFields": false,
"strictPropertyInitialization": false, "strictPropertyInitialization": false,
"importHelpers": true, "importHelpers": true,
// "noImplicitOverride": true,
"target": "ES2022", "target": "ES2022",
"module": "ES2022", "module": "ES2022",
"typeRoots": ["node_modules/@types"], "typeRoots": ["node_modules/@types"],