diff --git a/src/app/components/shared/_dialogs/create-comment/create-comment.component.ts b/src/app/components/shared/_dialogs/create-comment/create-comment.component.ts index 12ddadb5c28685d76562b98182867fcaed028841..72c1bb989922404415c8e5d66158337929770773 100644 --- a/src/app/components/shared/_dialogs/create-comment/create-comment.component.ts +++ b/src/app/components/shared/_dialogs/create-comment/create-comment.component.ts @@ -1,16 +1,16 @@ import { Component, Inject, Input, OnInit, ViewChild } from '@angular/core'; -import { Comment, Language as CommentLanguage } from '../../../../models/comment'; +import { Comment } from '../../../../models/comment'; import { NotificationService } from '../../../../services/util/notification.service'; import { MAT_DIALOG_DATA, MatDialog, MatDialogRef } from '@angular/material/dialog'; import { TranslateService } from '@ngx-translate/core'; import { User } from '../../../../models/user'; import { EventService } from '../../../../services/util/event.service'; import { SpacyDialogComponent } from '../spacy-dialog/spacy-dialog.component'; -import { LanguagetoolService, Language, LanguagetoolResult } from '../../../../services/http/languagetool.service'; -import { CreateCommentKeywords } from '../../../../utils/create-comment-keywords'; +import { LanguagetoolService } from '../../../../services/http/languagetool.service'; +import { CreateCommentKeywords, KeywordsResultType } from '../../../../utils/create-comment-keywords'; import { WriteCommentComponent } from '../../write-comment/write-comment.component'; -import { DeepLDialogComponent } from '../deep-ldialog/deep-ldialog.component'; -import { DeepLService, SourceLang, TargetLang } from '../../../../services/http/deep-l.service'; +import { DeepLService } from '../../../../services/http/deep-l.service'; +import { SpacyService } from '../../../../services/http/spacy.service'; @Component({ selector: 'app-submit-comment', @@ -33,6 +33,7 @@ export class CreateCommentComponent implements OnInit { public dialog: MatDialog, public languagetoolService: LanguagetoolService, private deeplService: DeepLService, + private spacyService: SpacyService, public eventService: EventService, @Inject(MAT_DIALOG_DATA) public data: any) { } @@ -67,63 +68,30 @@ export class CreateCommentComponent implements OnInit { } openSpacyDialog(comment: Comment, rawText: string, forward: boolean): void { - CreateCommentKeywords.isSpellingAcceptable(this.languagetoolService, rawText, this.commentComponent.selectedLang) - .subscribe((result) => { - if (result.isAcceptable) { - if (forward) { - this.callDeepL(comment, result.text, result.result); - } else { - this.callSpacy(comment, result.text, result.result); - } - } else { - comment.language = CommentLanguage.auto; + CreateCommentKeywords.generateKeywords(this.languagetoolService, this.deeplService, + this.spacyService, comment.body, forward, this.commentComponent.selectedLang) + .subscribe(result => { + this.isSendingToSpacy = false; + comment.language = result.language; + comment.keywordsFromSpacy = result.keywords; + comment.keywordsFromQuestioner = []; + if (forward || + ((result.resultType === KeywordsResultType.failure) && !result.wasSpacyError) || + result.resultType === KeywordsResultType.badSpelled) { this.dialogRef.close(comment); - this.isSendingToSpacy = false; + } else { + const dialogRef = this.dialog.open(SpacyDialogComponent, { + data: { + result: result.resultType, + comment + } + }); + dialogRef.afterClosed().subscribe(dialogResult => { + if (dialogResult) { + this.dialogRef.close(dialogResult); + } + }); } - }, () => { - comment.language = CommentLanguage.auto; - this.dialogRef.close(comment); - this.isSendingToSpacy = false; }); } - - private callDeepL(comment: Comment, text: string, result: LanguagetoolResult) { - let target = TargetLang.EN_US; - const code = result.language.detectedLanguage.code.toUpperCase().split('-')[0]; - if (code.startsWith(SourceLang.EN)) { - target = TargetLang.DE; - } - DeepLDialogComponent.generateDeeplDelta(this.deeplService, comment.body, target) - .subscribe(([_, improvedText]) => { - this.callSpacy(comment, CreateCommentKeywords.escapeForSpacy(improvedText), result, true); - }, () => { - this.callSpacy(comment, text, result, true); - }); - } - - private callSpacy(comment: Comment, text: string, result: LanguagetoolResult, forward = false) { - const commentLang = this.languagetoolService.mapLanguageToSpacyModel(result.language.code as Language); - const selectedLangExtend = this.commentComponent.selectedLang[2] === '-' ? - this.commentComponent.selectedLang.substr(0, 2) : this.commentComponent.selectedLang; - // Store language if it was auto-detected - if (this.commentComponent.selectedLang === 'auto') { - comment.language = Comment.mapModelToLanguage(commentLang); - } else if (CommentLanguage[selectedLangExtend]) { - comment.language = CommentLanguage[selectedLangExtend]; - } - this.isSendingToSpacy = false; - const dialogRef = this.dialog.open(SpacyDialogComponent, { - data: { - comment, - commentLang, - commentBodyChecked: text, - forward - } - }); - dialogRef.afterClosed().subscribe(dialogResult => { - if (dialogResult) { - this.dialogRef.close(dialogResult); - } - }); - } } diff --git a/src/app/components/shared/_dialogs/deep-ldialog/deep-ldialog.component.ts b/src/app/components/shared/_dialogs/deep-ldialog/deep-ldialog.component.ts index 5844ca5d937fb344a7157ba2bbcd9112332d5b4c..9b43649b0438721375efb8dbf11e5cbe5a9512c9 100644 --- a/src/app/components/shared/_dialogs/deep-ldialog/deep-ldialog.component.ts +++ b/src/app/components/shared/_dialogs/deep-ldialog/deep-ldialog.component.ts @@ -5,9 +5,7 @@ import { NotificationService } from '../../../../services/util/notification.serv import { LanguageService } from '../../../../services/util/language.service'; import { TranslateService } from '@ngx-translate/core'; import { ExplanationDialogComponent } from '../explanation-dialog/explanation-dialog.component'; -import { DeepLService, FormalityType, TargetLang } from '../../../../services/http/deep-l.service'; -import { Observable } from 'rxjs'; -import { map } from 'rxjs/operators'; +import { DeepLService, FormalityType } from '../../../../services/http/deep-l.service'; import { CreateCommentKeywords } from '../../../../utils/create-comment-keywords'; export interface ResultValue { @@ -44,45 +42,6 @@ export class DeepLDialogComponent implements OnInit, AfterViewInit { this.supportsFormality = DeepLService.supportsFormality(this.data.target); } - public static generateDeeplDelta(deepl: DeepLService, body: string, targetLang: TargetLang, - formality = FormalityType.less): Observable<[string, string]> { - const delta = ViewCommentDataComponent.getDeltaFromData(body); - const xml = delta.ops.reduce((acc, e, i) => { - if (typeof e['insert'] === 'string' && e['insert'].trim().length) { - acc += '<x i="' + i + '">' + this.encodeHTML(CreateCommentKeywords.removeMarkdown(e['insert'])) + '</x>'; - e['insert'] = ''; - } - return acc; - }, ''); - return deepl.improveTextStyle(xml, targetLang, formality).pipe( - map(str => { - const regex = /<x i="(\d+)">([^<]+)<\/x>/gm; - let m; - while ((m = regex.exec(str)) !== null) { - delta.ops[+m[1]]['insert'] += this.decodeHTML(m[2]); - } - const text = delta.ops.reduce((acc, el) => acc + (typeof el['insert'] === 'string' ? el['insert'] : ''), ''); - return [ViewCommentDataComponent.getDataFromDelta(delta), text]; - }) - ); - } - - private static encodeHTML(str: string): string { - return str.replace(/&/g, '&') - .replace(/</g, '<') - .replace(/>/g, '>') - .replace(/"/g, '"') - .replace(/'/g, '''); - } - - private static decodeHTML(str: string): string { - return str.replace(/'/g, '\'') - .replace(/"/g, '"') - .replace(/>/g, '>') - .replace(/</g, '<') - .replace(/&/g, '&'); - } - ngOnInit(): void { this.translateService.use(localStorage.getItem('currentLang')); this.normalValue = { @@ -138,7 +97,7 @@ export class DeepLDialogComponent implements OnInit, AfterViewInit { } onFormalityChange(formality: string) { - DeepLDialogComponent.generateDeeplDelta(this.deeplService, this.data.body, this.data.usedTarget, formality as FormalityType) + CreateCommentKeywords.generateDeeplDelta(this.deeplService, this.data.body, this.data.usedTarget, formality as FormalityType) .subscribe(([improvedBody, improvedText]) => { this.improvedValue.body = improvedBody; this.improvedValue.text = improvedText; diff --git a/src/app/components/shared/_dialogs/spacy-dialog/spacy-dialog.component.html b/src/app/components/shared/_dialogs/spacy-dialog/spacy-dialog.component.html index ed46fd8e49743888ad00a1952d5792ec99713899..a3d1da967a308ba10454e0168f315067ee3885cb 100644 --- a/src/app/components/shared/_dialogs/spacy-dialog/spacy-dialog.component.html +++ b/src/app/components/shared/_dialogs/spacy-dialog/spacy-dialog.component.html @@ -23,9 +23,6 @@ </ars-row> </span> <ars-row class="list-container"> - <div fxLayout="row" fxLayoutAlign="center center" fxFill> - <app-mat-spinner-overlay *ngIf="isLoading"></app-mat-spinner-overlay> - </div> <mat-list dense class="keywords-list"> <mat-list-item *ngFor="let keyword of keywords; let odd = odd; let even = even; let i = index" [class.keywords-alternate]="odd" @@ -59,7 +56,7 @@ </mat-list> </ars-row> <ars-row> - <span *ngIf="!isLoading && (!langSupported || !hasKeywordsFromSpacy)"> + <span *ngIf="!langSupported || !hasKeywordsFromSpacy"> <p class="manual-input-title">{{ 'spacy-dialog.add-manually' | translate }}</p> <textarea class="manual-input" [(ngModel)]="manualKeywords" (input)="manualKeywordsToKeywords()"></textarea> </span> @@ -68,7 +65,7 @@ </ars-row> <ars-row ars-flex-box class="action-button-container"> - <ars-col *ngIf="!isLoading && langSupported && hasKeywordsFromSpacy"> + <ars-col *ngIf="langSupported && hasKeywordsFromSpacy"> <button mat-flat-button class="help-button" @@ -77,7 +74,7 @@ {{ 'explanation.label' | translate}} </button> </ars-col> - <ars-fill *ngIf="isLoading || !langSupported || !hasKeywordsFromSpacy"> + <ars-fill *ngIf="!langSupported || !hasKeywordsFromSpacy"> </ars-fill> <ars-col> <app-dialog-action-buttons @@ -87,7 +84,7 @@ [showDivider]="false" [spacing]="false" [cancelButtonClickAction]="buildCloseDialogActionCallback()" - [confirmButtonClickAction]="!isLoading ? buildCreateCommentActionCallback() : undefined"> + [confirmButtonClickAction]="buildCreateCommentActionCallback()"> </app-dialog-action-buttons> </ars-col> </ars-row> diff --git a/src/app/components/shared/_dialogs/spacy-dialog/spacy-dialog.component.ts b/src/app/components/shared/_dialogs/spacy-dialog/spacy-dialog.component.ts index d31762cc223b63b29718e60b93256b1615de0635..0b478d36cacf7074b641a9482d0153811b487f04 100644 --- a/src/app/components/shared/_dialogs/spacy-dialog/spacy-dialog.component.ts +++ b/src/app/components/shared/_dialogs/spacy-dialog/spacy-dialog.component.ts @@ -1,11 +1,10 @@ -import { AfterContentInit, Component, Inject, OnInit, ViewChild } from '@angular/core'; +import { Component, Inject, OnInit, ViewChild } from '@angular/core'; import { MatDialogRef, MAT_DIALOG_DATA, MatDialog } from '@angular/material/dialog'; -import { SpacyService, SpacyKeyword } from '../../../../services/http/spacy.service'; -import { LanguagetoolService } from '../../../../services/http/languagetool.service'; +import { SpacyKeyword } from '../../../../services/http/spacy.service'; import { Comment } from '../../../../models/comment'; import { DialogActionButtonsComponent } from '../../dialog/dialog-action-buttons/dialog-action-buttons.component'; -import { Model } from '../../../../services/http/spacy.interface'; import { ExplanationDialogComponent } from '../explanation-dialog/explanation-dialog.component'; +import { KeywordsResultType } from '../../../../utils/create-comment-keywords'; export interface Keyword { word: string; @@ -20,24 +19,19 @@ export interface Keyword { templateUrl: './spacy-dialog.component.html', styleUrls: ['./spacy-dialog.component.scss'] }) -export class SpacyDialogComponent implements OnInit, AfterContentInit { +export class SpacyDialogComponent implements OnInit { @ViewChild('appDialogActionButtons') appDialogActionButtons: DialogActionButtonsComponent; comment: Comment; - commentLang: Model; - commentBodyChecked: string; keywords: Keyword[] = []; keywordsOriginal: SpacyKeyword[] = []; hasKeywordsFromSpacy = false; - isLoading = false; langSupported: boolean; manualKeywords = ''; _concurrentEdits = 0; constructor( - protected langService: LanguagetoolService, - private spacyService: SpacyService, public dialogRef: MatDialogRef<SpacyDialogComponent>, private dialog: MatDialog, @Inject(MAT_DIALOG_DATA) public data) { @@ -45,24 +39,19 @@ export class SpacyDialogComponent implements OnInit, AfterContentInit { ngOnInit(): void { this.comment = this.data.comment; - this.commentLang = this.data.commentLang; - this.commentBodyChecked = this.data.commentBodyChecked; - this.langSupported = this.langService.isSupportedLanguage(this.data.commentLang); + this.langSupported = this.data.result !== KeywordsResultType.languageNotSupported; + this.hasKeywordsFromSpacy = this.data.result === KeywordsResultType.successful && + this.comment.keywordsFromSpacy.length > 0; + this.keywords = this.comment.keywordsFromSpacy.map(keyword => ({ + word: keyword.text, + dep: [...keyword.dep], + completed: false, + editing: false, + selected: false + } as Keyword)); + this.keywords.sort((a, b) => a.word.localeCompare(b.word)); } - ngAfterContentInit(): void { - if (this.langSupported) { - this.evalInput(this.commentLang); - } else if (this.data.forward) { - this.keywords = []; - this.keywordsOriginal = []; - setTimeout(() => this.buildCreateCommentActionCallback()()); - } - } - - /** - * Returns a lambda which closes the dialog on call. - */ buildCloseDialogActionCallback(): () => void { return () => this.dialogRef.close(); } @@ -78,39 +67,6 @@ export class SpacyDialogComponent implements OnInit, AfterContentInit { }; } - evalInput(model: Model) { - this.isLoading = true; - const afterFinish = () => { - if (this.data.forward) { - this.buildCreateCommentActionCallback()(); - } - }; - - // N at first pos = all Nouns(NN de/en) including singular(NN, NNP en), plural (NNPS, NNS en), proper Noun(NNE, NE de) - this.spacyService.getKeywords(this.commentBodyChecked, model) - .subscribe(words => { - this.keywordsOriginal = words; - this.keywords = words.map(keyword => ({ - word: keyword.text, - dep: [...keyword.dep], - completed: false, - editing: false, - selected: false - } as Keyword)); - this.keywords.sort((a, b) => a.word.localeCompare(b.word)); - this.hasKeywordsFromSpacy = this.keywords.length > 0; - }, () => { - this.keywords = []; - this.keywordsOriginal = []; - this.hasKeywordsFromSpacy = false; - this.isLoading = false; - afterFinish(); - }, () => { - this.isLoading = false; - afterFinish(); - }); - } - onEdit(keyword) { keyword.editing = true; keyword.completed = false; diff --git a/src/app/components/shared/_dialogs/worker-dialog/worker-dialog-task.ts b/src/app/components/shared/_dialogs/worker-dialog/worker-dialog-task.ts index 8298e161b3516dd5e71d37cf28a102b586f09b2e..35607461c19c1bbf527c5010d9d2965207b0d316 100644 --- a/src/app/components/shared/_dialogs/worker-dialog/worker-dialog-task.ts +++ b/src/app/components/shared/_dialogs/worker-dialog/worker-dialog-task.ts @@ -3,26 +3,20 @@ import { SpacyKeyword, SpacyService } from '../../../../services/http/spacy.serv import { CommentService } from '../../../../services/http/comment.service'; import { Comment, Language } from '../../../../models/comment'; import { Language as Lang, LanguagetoolService } from '../../../../services/http/languagetool.service'; -import { CreateCommentKeywords } from '../../../../utils/create-comment-keywords'; +import { CreateCommentKeywords, KeywordsResult, KeywordsResultType } from '../../../../utils/create-comment-keywords'; import { TSMap } from 'typescript-map'; import { HttpErrorResponse } from '@angular/common/http'; -import { CURRENT_SUPPORTED_LANGUAGES, Model } from '../../../../services/http/spacy.interface'; -import { ViewCommentDataComponent } from '../../view-comment-data/view-comment-data.component'; +import { DeepLService } from '../../../../services/http/deep-l.service'; const concurrentCallsPerTask = 4; -enum FinishType { - completed, - badSpelled, - failed -} - export class WorkerDialogTask { error: string = null; readonly statistics = { succeeded: 0, badSpelled: 0, + notSupported: 0, failed: 0, length: 0 }; @@ -32,6 +26,7 @@ export class WorkerDialogTask { constructor(public readonly room: Room, private comments: Comment[], private spacyService: SpacyService, + private deeplService: DeepLService, private commentService: CommentService, private languagetoolService: LanguagetoolService, private finished: () => void) { @@ -60,48 +55,43 @@ export class WorkerDialogTask { return; } const currentComment = this._comments[currentIndex]; - const text = ViewCommentDataComponent.getTextFromData(currentComment.body); - CreateCommentKeywords.isSpellingAcceptable(this.languagetoolService, text) - .subscribe(result => { - if (!result.isAcceptable) { - this.finishSpacyCall(FinishType.badSpelled, currentIndex); - return; - } - const commentModel = currentComment.language.toLowerCase(); - const model = commentModel !== 'auto' ? commentModel.toLowerCase() as Model : - this.languagetoolService.mapLanguageToSpacyModel(result.result.language.detectedLanguage.code as Lang); - if (model === 'auto' || !CURRENT_SUPPORTED_LANGUAGES.includes(model)) { - this.finishSpacyCall(FinishType.badSpelled, currentIndex); - return; - } - this.spacyService.getKeywords(result.text, model) - .subscribe(newKeywords => - this.finishSpacyCall(FinishType.completed, currentIndex, newKeywords, model.toUpperCase() as Language), - __ => this.finishSpacyCall(FinishType.failed, currentIndex)); - }, _ => this.finishSpacyCall(FinishType.failed, currentIndex)); + CreateCommentKeywords.generateKeywords(this.languagetoolService, this.deeplService, this.spacyService, + currentComment.body, + !currentComment.keywordsFromQuestioner || currentComment.keywordsFromQuestioner.length === 0, + currentComment.language.toLowerCase() as Lang) + .subscribe((result) => this.finishSpacyCall(currentIndex, result, currentComment.language)); } - private finishSpacyCall(finishType: FinishType, index: number, tags?: SpacyKeyword[], lang?: Language): void { - if (finishType === FinishType.completed) { - this.patchToServer(tags, index, lang); - } else if (finishType === FinishType.badSpelled) { + private finishSpacyCall(index: number, result: KeywordsResult, previous: Language): void { + let undo: () => any = () => ''; + if (result.resultType === KeywordsResultType.badSpelled) { this.statistics.badSpelled++; - this.patchToServer([], index, Language.auto); - } else { + undo = () => this.statistics.badSpelled--; + } else if (result.resultType === KeywordsResultType.languageNotSupported) { + this.statistics.notSupported++; + undo = () => this.statistics.notSupported--; + } else if (result.resultType === KeywordsResultType.failure) { this.statistics.failed++; - this.patchToServer([], index, Language.auto); + undo = () => this.statistics.failed--; + } + if (result.language === Language.auto) { + result.language = null; } + this.patchToServer(result.keywords, index, result.language, undo); } - private patchToServer(tags: SpacyKeyword[], index: number, language: Language) { + private patchToServer(tags: SpacyKeyword[], index: number, language: Language, undo: () => any) { const changes = new TSMap<string, string>(); changes.set('keywordsFromSpacy', JSON.stringify(tags)); - changes.set('language', language); + if (language !== null) { + changes.set('language', language); + } this.commentService.patchComment(this._comments[index], changes).subscribe(_ => { this.statistics.succeeded++; this.callSpacy(index + concurrentCallsPerTask); }, patchError => { + undo(); this.statistics.failed++; if (patchError instanceof HttpErrorResponse && patchError.status === 403) { this.error = 'forbidden'; diff --git a/src/app/components/shared/_dialogs/worker-dialog/worker-dialog.component.ts b/src/app/components/shared/_dialogs/worker-dialog/worker-dialog.component.ts index 13dfc7c5063776a8323006f55193fb1fbbc8f05f..2054bfece2147aad01ba4f1804bbc54f0bc257df 100644 --- a/src/app/components/shared/_dialogs/worker-dialog/worker-dialog.component.ts +++ b/src/app/components/shared/_dialogs/worker-dialog/worker-dialog.component.ts @@ -10,6 +10,7 @@ import { TranslateService } from '@ngx-translate/core'; import { LanguageService } from '../../../../services/util/language.service'; import { Comment, Language } from '../../../../models/comment'; import { RoomDataService } from '../../../../services/util/room-data.service'; +import { DeepLService } from '../../../../services/http/deep-l.service'; @Component({ selector: 'app-worker-dialog', @@ -27,6 +28,7 @@ export class WorkerDialogComponent implements OnInit { private languagetoolService: LanguagetoolService, private spacyService: SpacyService, protected langService: LanguageService, + private deepLService: DeepLService, private translateService: TranslateService, private roomDataService: RoomDataService) { langService.langEmitter.subscribe(lang => translateService.use(lang)); @@ -112,14 +114,15 @@ export class WorkerDialogComponent implements OnInit { appendRoom(room: Room, comments: Comment[]) { WorkerDialogComponent.queuedRooms.set(room.id, - new WorkerDialogTask(room, comments, this.spacyService, this.commentService, this.languagetoolService, () => { - setTimeout(() => { - WorkerDialogComponent.queuedRooms.delete(room.id); - if (WorkerDialogComponent.queuedRooms.length === 0) { - this.close(); - } - }, 10_000); - }) + new WorkerDialogTask(room, comments, this.spacyService, this.deepLService, this.commentService, + this.languagetoolService, () => { + setTimeout(() => { + WorkerDialogComponent.queuedRooms.delete(room.id); + if (WorkerDialogComponent.queuedRooms.length === 0) { + this.close(); + } + }, 10_000); + }) ); } diff --git a/src/app/components/shared/header/header.component.ts b/src/app/components/shared/header/header.component.ts index a8d0271cdca2525269378cc2e0eb4b6fc8db7050..2e64ca1d4f4d8723cc9111939fc94da4b59a51bb 100644 --- a/src/app/components/shared/header/header.component.ts +++ b/src/app/components/shared/header/header.component.ts @@ -145,6 +145,7 @@ export class HeaderComponent implements OnInit,AfterViewInit { localStorage.setItem('shortId', this.shortId); this.roomService.getRoomByShortId(this.shortId).subscribe(room => { this.room = room; + this.moderationEnabled = !room.directSend; this._subscriptionRoomService = this.wsRoomService.getRoomStream(this.room.id).subscribe(msg => { const message = JSON.parse(msg.body); if (message.type === 'RoomPatched') { diff --git a/src/app/components/shared/write-comment/write-comment.component.ts b/src/app/components/shared/write-comment/write-comment.component.ts index 5e0e5152cfa92f6caefa8d8513734ee26bee29a8..4008918a5ab59512872d50224ca7e515a9e9ad9c 100644 --- a/src/app/components/shared/write-comment/write-comment.component.ts +++ b/src/app/components/shared/write-comment/write-comment.component.ts @@ -9,6 +9,7 @@ import { DeepLService, SourceLang, TargetLang } from '../../../services/http/dee import { DeepLDialogComponent, ResultValue } from '../_dialogs/deep-ldialog/deep-ldialog.component'; import { MatDialog } from '@angular/material/dialog'; import { FormControl, Validators } from '@angular/forms'; +import { CreateCommentKeywords } from '../../../utils/create-comment-keywords'; type SubmitFunction = (commentData: string, commentText: string, selectedTag: string, name?: string, verifiedWithoutDeepl?: boolean) => any; @@ -159,7 +160,7 @@ export class WriteCommentComponent implements OnInit { if (code.startsWith(SourceLang.EN)) { target = TargetLang.DE; } - DeepLDialogComponent.generateDeeplDelta(this.deeplService, body, target) + CreateCommentKeywords.generateDeeplDelta(this.deeplService, body, target) .subscribe(([improvedBody, improvedText]) => { this.isSpellchecking = false; if (improvedText.replace(/\s+/g, '') === text.replace(/\s+/g, '')) { diff --git a/src/app/services/http/base-http.service.ts b/src/app/services/http/base-http.service.ts index 77b29ce5b2122f95bdb86e5220c6da08eee2788c..520bf005c8cdf626a178f063c8cc25aea692e27f 100644 --- a/src/app/services/http/base-http.service.ts +++ b/src/app/services/http/base-http.service.ts @@ -21,7 +21,7 @@ export class BaseHttpService { this.nextRequest = new Date().getTime() + 86_400_000; } } - console.error(error); + console.error(operation, error); return throwError(error); }; } diff --git a/src/app/utils/create-comment-keywords.ts b/src/app/utils/create-comment-keywords.ts index e827d8c1c5b330c1b8495f0b9cc78a572cbe6378..9ba833a870ee2d266fa01a7abe411d47f1c6060c 100644 --- a/src/app/utils/create-comment-keywords.ts +++ b/src/app/utils/create-comment-keywords.ts @@ -1,5 +1,29 @@ -import { Language, LanguagetoolService } from '../services/http/languagetool.service'; -import { map } from 'rxjs/operators'; +import { Language, LanguagetoolResult, LanguagetoolService } from '../services/http/languagetool.service'; +import { catchError, map, switchMap } from 'rxjs/operators'; +import { Observable, of } from 'rxjs'; +import { SpacyKeyword, SpacyService } from '../services/http/spacy.service'; +import { DeepLService, FormalityType, SourceLang, TargetLang } from '../services/http/deep-l.service'; +import { Comment, Language as CommentLanguage } from '../models/comment'; +import { ViewCommentDataComponent } from '../components/shared/view-comment-data/view-comment-data.component'; +import { CURRENT_SUPPORTED_LANGUAGES, Model } from '../services/http/spacy.interface'; + +export enum KeywordsResultType { + successful, + badSpelled, + languageNotSupported, + failure +} + +export interface KeywordsResult { + keywords: SpacyKeyword[]; + language: CommentLanguage; + resultType: KeywordsResultType; + error?: any; + wasSpacyError?: boolean; +} + +const ERROR_QUOTIENT_WELL_SPELLED = 20; +const ERROR_QUOTIENT_USE_DEEPL = 75; export class CreateCommentKeywords { @@ -14,22 +38,126 @@ export class CreateCommentKeywords { .replace(/\[([^\n\[\]]*)\]\(([^()\n]*)\)/gm, '$1 $2'); } - static isSpellingAcceptable(languagetoolService: LanguagetoolService, text: string, language: Language = 'auto') { - return languagetoolService.checkSpellings(text, language).pipe( - map(result => { - const wordCount = text.trim().split(' ').length; - const hasConfidence = language === 'auto' ? result.language.detectedLanguage.confidence >= 0.5 : true; - const hasLessMistakes = (result.matches.length * 100) / wordCount <= 50; - return { - isAcceptable: hasConfidence && hasLessMistakes, - text: this.escapeForSpacy(text), - result - }; + public static generateDeeplDelta(deepl: DeepLService, body: string, targetLang: TargetLang, + formality = FormalityType.less): Observable<[string, string]> { + const delta = ViewCommentDataComponent.getDeltaFromData(body); + const xml = delta.ops.reduce((acc, e, i) => { + if (typeof e['insert'] === 'string' && e['insert'].trim().length) { + acc += '<x i="' + i + '">' + this.encodeHTML(CreateCommentKeywords.removeMarkdown(e['insert'])) + '</x>'; + e['insert'] = ''; + } + return acc; + }, ''); + return deepl.improveTextStyle(xml, targetLang, formality).pipe( + map(str => { + const regex = /<x i="(\d+)">([^<]+)<\/x>/gm; + let m; + while ((m = regex.exec(str)) !== null) { + delta.ops[+m[1]]['insert'] += this.decodeHTML(m[2]); + } + const text = delta.ops.reduce((acc, el) => acc + (typeof el['insert'] === 'string' ? el['insert'] : ''), ''); + return [ViewCommentDataComponent.getDataFromDelta(delta), text]; }) ); } - static escapeForSpacy(text: string): string { + static generateKeywords(languagetoolService: LanguagetoolService, + deeplService: DeepLService, + spacyService: SpacyService, + body: string, + useDeepl: boolean = false, + language: Language = 'auto'): Observable<KeywordsResult> { + const text = ViewCommentDataComponent.getTextFromData(body); + return languagetoolService.checkSpellings(text, language).pipe( + switchMap(result => this.spacyKeywordsFromLanguagetoolResult(languagetoolService, deeplService, + spacyService, text, body, language, result, useDeepl)), + catchError((err) => of({ + keywords: [], + language: CommentLanguage.auto, + resultType: KeywordsResultType.failure, + error: err + } as KeywordsResult)) + ); + } + + private static spacyKeywordsFromLanguagetoolResult(languagetoolService: LanguagetoolService, + deeplService: DeepLService, + spacyService: SpacyService, + text: string, + body: string, + selectedLanguage: Language, + result: LanguagetoolResult, + useDeepl: boolean): Observable<KeywordsResult> { + const wordCount = text.trim().split(' ').length; + const hasConfidence = selectedLanguage === 'auto' ? result.language.detectedLanguage.confidence >= 0.5 : true; + const errorQuotient = (result.matches.length * 100) / wordCount; + console.log(errorQuotient); + if (!hasConfidence || + errorQuotient > ERROR_QUOTIENT_USE_DEEPL || + (!useDeepl && errorQuotient > ERROR_QUOTIENT_WELL_SPELLED)) { + return of({ + keywords: [], + language: CommentLanguage.auto, + resultType: KeywordsResultType.badSpelled + } as KeywordsResult); + } + const escapedText = this.escapeForSpacy(text); + let textLangObservable = of(escapedText); + if (useDeepl && errorQuotient > ERROR_QUOTIENT_WELL_SPELLED) { + let target = TargetLang.EN_US; + const code = result.language.detectedLanguage.code.toUpperCase().split('-')[0]; + if (code.startsWith(SourceLang.EN)) { + target = TargetLang.DE; + } + textLangObservable = this.generateDeeplDelta(deeplService, body, target) + .pipe( + map(([_, improvedText]) => this.escapeForSpacy(improvedText)) + ); + } + return textLangObservable.pipe( + switchMap((textForSpacy) => this.callSpacy(spacyService, textForSpacy, + languagetoolService.isSupportedLanguage(result.language.code as Language), selectedLanguage, + languagetoolService.mapLanguageToSpacyModel(result.language.code as Language))) + ); + } + + private static callSpacy(spacyService: SpacyService, + text: string, + isResultLangSupported: boolean, + selectedLanguage: Language, + commentModel: Model): Observable<KeywordsResult> { + const selectedLangExtend = + selectedLanguage[2] === '-' ? selectedLanguage.substr(0, 2) : selectedLanguage; + let finalLanguage: CommentLanguage; + if (selectedLanguage === 'auto') { + finalLanguage = Comment.mapModelToLanguage(commentModel); + } else if (CommentLanguage[selectedLangExtend]) { + finalLanguage = CommentLanguage[selectedLangExtend]; + } + if (!isResultLangSupported || !CURRENT_SUPPORTED_LANGUAGES.includes(commentModel)) { + return of({ + keywords: [], + language: finalLanguage, + resultType: KeywordsResultType.languageNotSupported + } as KeywordsResult); + } + return spacyService.getKeywords(text, commentModel).pipe( + map(keywords => ({ + keywords, + language: finalLanguage, + resultType: KeywordsResultType.successful + } as KeywordsResult)), + catchError(err => of({ + keywords: [], + language: finalLanguage, + resultType: KeywordsResultType.failure, + error: err, + wasSpacyError: true + } as KeywordsResult)) + ); + } + + private static escapeForSpacy(text: string): string { text = this.makeCapslockLowercase(text); return text.replace(/\(([^-\s)]+-)\)([^\s]+)/gmi, '$1$2'); } @@ -55,4 +183,20 @@ export class CreateCommentKeywords { } return result; } + + private static encodeHTML(str: string): string { + return str.replace(/&/g, '&') + .replace(/</g, '<') + .replace(/>/g, '>') + .replace(/"/g, '"') + .replace(/'/g, '''); + } + + private static decodeHTML(str: string): string { + return str.replace(/'/g, '\'') + .replace(/"/g, '"') + .replace(/>/g, '>') + .replace(/</g, '<') + .replace(/&/g, '&'); + } }