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...');
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();

View File

@ -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');
},
},
],

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 { 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() {

View File

@ -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<unknown>;
#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;
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<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();
console.log(file);
console.log(this._fileDataService.annotations());

View File

@ -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');
},
},
],

View File

@ -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] }),
});
}
}

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 { 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<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 { 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<boolean> {
override async canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Promise<boolean> {
const currentUser = this._userService.currentUser;
if (!currentUser?.hasAnyRole) {

View File

@ -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<IIqserUser, User> {
protected readonly _defaultModelPath = 'user';
protected readonly _entityClass = User;
async loadCurrentUser(): Promise<User | undefined> {
override async loadCurrentUser(): Promise<User | undefined> {
const currentUser = await super.loadCurrentUser();
this._permissionsService.add({
@ -23,12 +23,12 @@ export class UserService extends IqserUserService<IIqserUser, User> {
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<IIqserUser, User> {
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() {

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

View File

@ -19,9 +19,9 @@ export class TrashDossier extends TrashItem implements Partial<IDossier> {
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);

View File

@ -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<IFile> {
readonly type = 'file';
@ -22,9 +22,9 @@ export class TrashFile extends TrashItem implements Partial<IFile> {
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,
) {

View File

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