diff --git a/proxy.conf.json b/proxy.conf.json index ea2af167e93cb8a36258701a4ad8d33a48dd4e9b..603e23b9ea844bcf7f57f2590ea71217ea63268e 100644 --- a/proxy.conf.json +++ b/proxy.conf.json @@ -9,7 +9,7 @@ "logLevel": "debug" }, "/spacy": { - "target": "https://spacy.frag.jetzt/dep", + "target": "https://spacy.frag.jetzt/noun", "secure": true, "changeOrigin": true, "pathRewrite": { 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 8191bdb32c6625bf4a489cc8f837df27293d9885..0aed919adc5dbe57e5d190deffd2785a309e04be 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 @@ -34,7 +34,7 @@ export class CreateCommentComponent implements OnInit, OnDestroy { isSpellchecking = false; hasSpellcheckConfidence = true; - @ViewChild('commentBody', { static: true }) commentBody: HTMLDivElement; + @ViewChild('commentBody', {static: true}) commentBody: HTMLDivElement; constructor( private notification: NotificationService, @@ -72,7 +72,8 @@ export class CreateCommentComponent implements OnInit, OnDestroy { onNoClick(): void { this.dialogRef.close(); } - clearHTML(e){ + + clearHTML(e) { e.preventDefault(); const text = e.clipboardData.getData('text'); document.getElementById('answer-input').innerText += text.replace(/<[^>]*>?/gm, ''); @@ -108,19 +109,13 @@ export class CreateCommentComponent implements OnInit, OnDestroy { const hasSpellcheckConfidence = this.checkLanguageConfidence(res); if (hasSpellcheckConfidence && errorQuotient <= 20) { - let commentBodyChecked = this.inputText; const commentLang = this.languagetoolService.mapLanguageToSpacyModel(res.language.code); - for (let i = res.matches.length - 1; i >= 0; i--) { - commentBodyChecked = commentBodyChecked.substr(0, res.matches[i].offset) + - commentBodyChecked.substr(res.matches[i].offset + res.matches[i].length, commentBodyChecked.length); - } - const dialogRef = this.dialog.open(SpacyDialogComponent, { data: { comment, commentLang, - commentBodyChecked + commentBodyChecked: this.inputText } }); @@ -163,8 +158,8 @@ export class CreateCommentComponent implements OnInit, OnDestroy { commentBody.innerText = commentBody.innerText.slice(0, 500); } this.body = commentBody.innerText; - if(this.body.length === 1 && this.body.charCodeAt(this.body.length - 1) === 10){ - commentBody.innerHTML = commentBody.innerHTML.replace('<br>',''); + if (this.body.length === 1 && this.body.charCodeAt(this.body.length - 1) === 10) { + commentBody.innerHTML = commentBody.innerHTML.replace('<br>', ''); } this.inputText = commentBody.innerText; } @@ -175,7 +170,7 @@ export class CreateCommentComponent implements OnInit, OnDestroy { this.isSpellchecking = true; this.hasSpellcheckConfidence = true; this.checkSpellings(commentBody.innerText).subscribe((wordsCheck) => { - if(!this.checkLanguageConfidence(wordsCheck)) { + if (!this.checkLanguageConfidence(wordsCheck)) { this.hasSpellcheckConfidence = false; return; } @@ -210,14 +205,14 @@ export class CreateCommentComponent implements OnInit, OnDestroy { } const replacement = - '<div class="markUp" data-id="'+i+'" style="position: relative; display: inline-block; border-bottom: 1px dotted black">' + - '<span data-id="' + i + '" style="text-decoration: underline wavy red; cursor: pointer;">' + - wrongWord + - '</span>' + - // eslint-disable-next-line max-len - '<div class="dropdownBlock" style="display: none; width: 160px; background-color: white; border-style: solid; border-color: var(--primary); color: #fff; text-align: center; border-radius: 6px; padding: 5px 0; position: absolute; z-index: 1000; bottom: 100%;">' + - suggestionsHTML + - '</div>' + + '<div class="markUp" data-id="' + i + '" style="position: relative; display: inline-block; border-bottom: 1px dotted black">' + + '<span data-id="' + i + '" style="text-decoration: underline wavy red; cursor: pointer;">' + + wrongWord + + '</span>' + + // eslint-disable-next-line max-len + '<div class="dropdownBlock" style="display: none; width: 160px; background-color: white; border-style: solid; border-color: var(--primary); color: #fff; text-align: center; border-radius: 6px; padding: 5px 0; position: absolute; z-index: 1000; bottom: 100%;">' + + suggestionsHTML + + '</div>' + '</div>'; commentBody.innerHTML = commentBody.innerHTML.substr(0, res.matches[i].offset) + @@ -255,7 +250,8 @@ export class CreateCommentComponent implements OnInit, OnDestroy { }, 500); }); } - }, () => {}, () => { + }, () => { + }, () => { this.isSpellchecking = false; }); } 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 4a69313c912f7eafc40aff1070177819db8c91c6..06ee485a9838ce22483b93f1a3bf55b3ea2111ed 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 @@ -61,31 +61,20 @@ export class SpacyDialogComponent implements OnInit, AfterContentInit { evalInput(model: Model) { const keywords: Keyword[] = []; - let regex; - if(this.commentLang === 'de') { - regex = new RegExp('(?!Der|Die|Das)[A-ZAÄÖÜ][a-zäöüß]+(-[A-Z][a-zäöüß]+)*', 'g'); - } else if (this.commentLang === 'en') { - regex = new RegExp('(?!he|she|it|for|with)[a-z]{2,}(-[a-z]{2,})*', 'gi'); - } else { - regex = new RegExp('(?!au|de|la|le|en|un)[A-ZÀ-Ÿ]{2,}', 'gi'); - } this.isLoading = true; // 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 => { - for(const word of words) { - const filteredwords = word.match(regex) || []; - for (const filteredword of filteredwords) { - if(filteredword !== null && filteredword !== undefined && keywords.filter(item => item.word === filteredword).length < 1) { - keywords.push({ - word: filteredword, - completed: false, - editing: false, - selected: false - }); - } + for (const word of words) { + if (keywords.findIndex(item => item.word === word) < 0) { + keywords.push({ + word, + completed: false, + editing: false, + selected: false + }); } } this.keywords = keywords; diff --git a/src/app/components/shared/tag-cloud/tag-cloud-pop-up/tag-cloud-pop-up.component.html b/src/app/components/shared/tag-cloud/tag-cloud-pop-up/tag-cloud-pop-up.component.html index d3ec02d4c83f2e4dcca2edd3026c76b81eaf7cfd..9e29f1cac807dc8adcc65c9bfbff9f3b8f130a97 100644 --- a/src/app/components/shared/tag-cloud/tag-cloud-pop-up/tag-cloud-pop-up.component.html +++ b/src/app/components/shared/tag-cloud/tag-cloud-pop-up/tag-cloud-pop-up.component.html @@ -1,6 +1,6 @@ <div #popupContainer class="popupContainer" - (focusout)="onFocus($event)" + (focusout)="onFocusOut()" tabindex="0"> <div> <div> diff --git a/src/app/components/shared/tag-cloud/tag-cloud-pop-up/tag-cloud-pop-up.component.ts b/src/app/components/shared/tag-cloud/tag-cloud-pop-up/tag-cloud-pop-up.component.ts index 14c25ed9563abcc51f7822890cac8215e348cf05..9310b86cdb21bc1090d44ebcfa5cf23fd834ef3d 100644 --- a/src/app/components/shared/tag-cloud/tag-cloud-pop-up/tag-cloud-pop-up.component.ts +++ b/src/app/components/shared/tag-cloud/tag-cloud-pop-up/tag-cloud-pop-up.component.ts @@ -4,11 +4,10 @@ import { LanguageService } from '../../../../services/util/language.service'; import { AuthenticationService } from '../../../../services/http/authentication.service'; import { User } from '../../../../models/user'; import { TagCloudDataService, TagCloudDataTagEntry } from '../../../../services/util/tag-cloud-data.service'; -import {Language, LanguagetoolService} from "../../../../services/http/languagetool.service"; -import {FormControl} from "@angular/forms"; -import {TSMap} from "typescript-map"; -import {CommentService} from "../../../../services/http/comment.service"; -import {Comment} from "../../../../models/comment"; +import { Language, LanguagetoolService } from '../../../../services/http/languagetool.service'; +import { FormControl } from '@angular/forms'; +import { TSMap } from 'typescript-map'; +import { CommentService } from '../../../../services/http/comment.service'; const CLOSE_TIME = 1500; @@ -26,22 +25,17 @@ export class TagCloudPopUpComponent implements OnInit, AfterViewInit { categories: string[]; timePeriodText: string; user: User; - private _popupHoverTimer: number; - private _popupCloseTimer: number; - languages: Language[] = ['de-DE', 'en-US', 'fr', 'auto']; selectedLang: Language = 'en-US'; spellingData: string[] = undefined; - tagReplacementInput: string; - wronglySpelledTag: string; - - + private _popupHoverTimer: number; + private _popupCloseTimer: number; constructor(private langService: LanguageService, private translateService: TranslateService, private authenticationService: AuthenticationService, private tagCloudDataService: TagCloudDataService, - private languagetoolService : LanguagetoolService, - private commentService : CommentService) { + private languagetoolService: LanguagetoolService, + private commentService: CommentService) { this.langService.langEmitter.subscribe(lang => { this.translateService.use(lang); }); @@ -62,34 +56,26 @@ export class TagCloudPopUpComponent implements OnInit, AfterViewInit { clearTimeout(this._popupCloseTimer); }); html.addEventListener('mouseleave', () => { - this._popupCloseTimer = setTimeout(() => { - this.close(); - }, CLOSE_TIME); + this.close(); }); } - onFocus(event) { - if (!this.popupContainer.nativeElement.contains(event.target)) { - this.close(); - } + onFocusOut() { + this.close(); } leave(): void { clearTimeout(this._popupHoverTimer); - clearTimeout(this._popupCloseTimer); - this._popupCloseTimer = setTimeout(() => { - this.close(); - }, CLOSE_TIME); + this.close(); } enter(elem: HTMLElement, tag: string, tagData: TagCloudDataTagEntry, hoverDelayInMs: number): void { this.spellingData = undefined; - this.checkSpellings(tag,).subscribe(correction => { + this.languagetoolService.checkSpellings(tag, this.selectedLang).subscribe(correction => { this.spellingData = []; - this.wronglySpelledTag = tag; - for(const match of correction.matches) { - if(match.replacements != null && match.replacements.length > 0){ - for(const replacement of match.replacements) { + for (const match of correction.matches) { + if (match.replacements != null && match.replacements.length > 0) { + for (const replacement of match.replacements) { this.spellingData.push(replacement.value); } } @@ -109,12 +95,46 @@ export class TagCloudPopUpComponent implements OnInit, AfterViewInit { addBlacklistWord(): void { this.tagCloudDataService.blockWord(this.tag); - this.close(); + this.close(false); } - close(): void { + close(addDelay = true): void { const html = this.popupContainer.nativeElement as HTMLDivElement; - html.classList.remove('up', 'down', 'right', 'left'); + clearTimeout(this._popupCloseTimer); + if (addDelay) { + if (html.contains(document.activeElement) && html !== document.activeElement) { + return; + } + this._popupCloseTimer = setTimeout(() => { + if (html.contains(document.activeElement) && html !== document.activeElement) { + return; + } + html.classList.remove('up', 'down', 'right', 'left'); + }, CLOSE_TIME); + } else { + html.classList.remove('up', 'down', 'right', 'left'); + } + } + + updateTag(): void { + const tagReplacementInput = this.replacementInput.value.trim(); + if (tagReplacementInput.length < 1 || tagReplacementInput === this.tag) { + return; + } + const renameKeyword = (elem: string, index: number, array: string[]) => { + if (elem === this.tag) { + array[index] = tagReplacementInput; + } + }; + this.tagData.comments.forEach(comment => { + const changes = new TSMap<string, any>(); + comment.keywordsFromQuestioner.forEach(renameKeyword); + changes.set('keywordsFromQuestioner', JSON.stringify(comment.keywordsFromQuestioner)); + comment.keywordsFromSpacy.forEach(renameKeyword); + changes.set('keywordsFromSpacy', JSON.stringify(comment.keywordsFromSpacy)); + this.commentService.patchComment(comment, changes).subscribe(); + }); + this.close(false); } private position(elem: HTMLElement) { @@ -257,34 +277,4 @@ export class TagCloudPopUpComponent implements OnInit, AfterViewInit { months }).subscribe(subscriber); } - checkSpellings(text: string, language: Language = this.selectedLang) { - return this.languagetoolService.checkSpellings(text, language); - } - updateTag(){ - this.tagReplacementInput = this.replacementInput.value; - this.tagData.comments.forEach(comment => { - const changes = new TSMap<string, any>(); - let keywords = comment.keywordsFromQuestioner; - for (let i = 0; i < keywords.length; i++){ - if (keywords[i].toLowerCase() === this.wronglySpelledTag.toLowerCase()){ - keywords[i] = this.tagReplacementInput.trim(); - console.log(keywords[i]); - } - } - changes.set('keywordsFromQuestioner', JSON.stringify(keywords)); - keywords = comment.keywordsFromSpacy; - for (let i = 0; i < keywords.length; i++){ - if (keywords[i].toLowerCase() === this.wronglySpelledTag.toLowerCase()){ - keywords[i] = this.tagReplacementInput.trim(); - console.log(keywords[i]+ 'z'); - } - } - changes.set('keywordsFromSpacy', JSON.stringify(keywords)); - this.commentService.patchComment(comment, changes).subscribe(rt => { - console.log('PATCHED .........................'); - }); - }); - console.log(this.tagReplacementInput); - this.close(); - } } diff --git a/src/app/services/http/spacy.service.ts b/src/app/services/http/spacy.service.ts index c57a390bf4d9bfa3a9388c09513fce2fdc3a3ebd..394000f9c7c237963f8200ee39dce98671221c43 100644 --- a/src/app/services/http/spacy.service.ts +++ b/src/app/services/http/spacy.service.ts @@ -4,76 +4,27 @@ import { Observable } from 'rxjs'; import { BaseHttpService } from './base-http.service'; import { catchError, map, tap } from 'rxjs/operators'; -export type Model = 'de' | 'en' | 'fr'; - -export class Result { - arcs: Arc[]; - words: Word[]; - - constructor( - arcs: Arc[] = [], - words: Word[] = [] - ) { - this.arcs = arcs; - this.words = words; - } - - static empty(): Result { - return new Result(); - } -} - -export class Word { - tag: string; - text: string; - - constructor( - tag: string, - text: string - ) { - this.tag = tag; - this.text = text; - } -} - -export class Noun { - text: string; - dependencyRelation: string; - - constructor( - text: string, - dependencyRelation: string - ) { - this.text = text; - this.dependencyRelation = dependencyRelation; - } -} - -export class Arc { - dir: string; - end: number; - label: string; - start: number; - text: string; - - constructor( - dir: string, - end: number, - label: string, - start: number, - text: string, - ) { - this.dir = dir; - this.end = end; - this.label = label; - this.start = start; - this.text = text; - } - +export type Model = 'de' | 'en' | 'fr' | 'es' | 'it' | 'nl' | 'pt'; + +//[B]egin, [I]nside, [O]utside or unset +type EntityPosition = 'B' | 'I' | 'O' | ''; + +interface NounToken { + dep: string; // dependency inside the sentence + // eslint-disable-next-line @typescript-eslint/naming-convention + entity_pos: EntityPosition; // entity position + // eslint-disable-next-line @typescript-eslint/naming-convention + entity_type: string; // entity type + lemma: string; // lemma of token + tag: string; // tag of token + text: string; // text of token } +type NounCompound = NounToken[]; +type NounCompoundList = NounCompound[]; const httpOptions = { + // eslint-disable-next-line @typescript-eslint/naming-convention headers: new HttpHeaders({'Content-Type': 'application/json'}) }; @@ -86,42 +37,44 @@ export class SpacyService extends BaseHttpService { super(); } - getKeywords(text: string, model: string): Observable<string[]> { - console.log(text); - return this.analyse(text, model).pipe( - tap(e => { - const nouns: Noun[] = []; - const absoluteNouns = e.words.filter((v, index) => { - v['index'] = index; - return v.tag.charAt(0) === 'N'; - }); - console.log(e); - for (const arc of e.arcs) { - const index = arc.dir.charAt(0) === 'r' ? arc.end : arc.start; - const elem = e.words[index]; - if (elem.tag.charAt(0) === 'N') { - while (absoluteNouns[0]['index'] < index) { - const current = absoluteNouns.splice(0, 1)[0]; - console.log(current); - nouns.push(new Noun(current['text'], 'ROOT')); - } - const actualNode = absoluteNouns.splice(0, 1)[0]; - console.log(actualNode); - console.assert(actualNode['index'] === index, 'Indices should be equal!'); - nouns.push(new Noun(elem.text, arc.label)); - } + private static processCompound(result: string[], data: NounCompound) { + let isInEntity = false; + let start = 0; + const pushNew = (i: number) => { + if (start < i) { + result.push(data.slice(start, i).reduce((acc, current) => acc + ' ' + current.lemma, '')); + start = i; + } + }; + data.forEach((noun, i) => { + if (noun.entity_pos === 'B' || (noun.entity_pos === 'I' && !isInEntity)) { + // entity begins + pushNew(i); + isInEntity = true; + } else if (isInEntity) { + if (noun.entity_pos === '' || noun.entity_pos === 'O') { + // entity ends + pushNew(i); + isInEntity = false; } - console.log(nouns); - }), - map(result => result.words.filter(v => v.tag.charAt(0) === 'N').map(v => v.text)) - ); + } + }); + pushNew(data.length); } - analyse(text: string, model: string): Observable<Result> { + getKeywords(text: string, model: Model): Observable<string[]> { const url = '/spacy'; - return this.http.post<Result>(url, {text, model}, httpOptions) + return this.http.post<NounCompoundList>(url, {text, model}, httpOptions) .pipe( - catchError(this.handleError<any>('analyse')) + tap(_ => ''), + catchError(this.handleError<any>('getKeywords')), + map((result: NounCompoundList) => { + const filteredNouns: string[] = []; + result.forEach(compound => { + SpacyService.processCompound(filteredNouns, compound); + }); + return filteredNouns; + }) ); } }