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 f91956bc6fa891459e7ccca3a836cfd4245a4714..992447b3c8cde1042d8017f3eb3ae6804bcd5aef 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 @@ -17,6 +17,8 @@ import { LanguagetoolService, Language } from '../../../../services/http/languag }) export class CreateCommentComponent implements OnInit, OnDestroy { + @ViewChild('commentBody', {static: true}) commentBody: HTMLDivElement; + comment: Comment; user: User; @@ -34,8 +36,6 @@ export class CreateCommentComponent implements OnInit, OnDestroy { isSpellchecking = false; hasSpellcheckConfidence = true; - @ViewChild('commentBody', { static: true }) commentBody: HTMLDivElement; - constructor( private notification: NotificationService, public dialogRef: MatDialogRef<CommentListComponent>, @@ -46,6 +46,7 @@ export class CreateCommentComponent implements OnInit, OnDestroy { public eventService: EventService, @Inject(MAT_DIALOG_DATA) public data: any) { } + ngOnInit() { this.translateService.use(localStorage.getItem('currentLang')); setTimeout(() => { @@ -71,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, ''); @@ -100,33 +102,27 @@ export class CreateCommentComponent implements OnInit, OnDestroy { } } - checkUTFEmoji(body: string): string{ - var regex = /(?:\:.*?\:|[\u2700-\u27bf]|(?:\ud83c[\udde6-\uddff]){2}|[\ud800-\udbff][\udc00-\udfff]|[\u0023-\u0039]\ufe0f?\u20e3|\u3299|\u3297|\u303d|\u3030|\u24c2|\ud83c[\udd70-\udd71]|\ud83c[\udd7e-\udd7f]|\ud83c\udd8e|\ud83c[\udd91-\udd9a]|\ud83c[\udde6-\uddff]|\ud83c[\ude01-\ude02]|\ud83c\ude1a|\ud83c\ude2f|\ud83c[\ude32-\ude3a]|\ud83c[\ude50-\ude51]|\u203c|\u2049|[\u25aa-\u25ab]|\u25b6|\u25c0|[\u25fb-\u25fe]|\u00a9|\u00ae|\u2122|\u2139|\ud83c\udc04|[\u2600-\u26FF]|\u2b05|\u2b06|\u2b07|\u2b1b|\u2b1c|\u2b50|\u2b55|\u231a|\u231b|\u2328|\u23cf|[\u23e9-\u23f3]|[\u23f8-\u23fa]|\ud83c\udccf|\u2934|\u2935|[\u2190-\u21ff])/g; - - return body.replace(regex, ''); + checkUTFEmoji(body: string): string { + const regex = /(?:\:.*?\:|[\u2700-\u27bf]|(?:\ud83c[\udde6-\uddff]){2}|[\ud800-\udbff][\udc00-\udfff]|[\u0023-\u0039]\ufe0f?\u20e3|\u3299|\u3297|\u303d|\u3030|\u24c2|\ud83c[\udd70-\udd71]|\ud83c[\udd7e-\udd7f]|\ud83c\udd8e|\ud83c[\udd91-\udd9a]|\ud83c[\udde6-\uddff]|\ud83c[\ude01-\ude02]|\ud83c\ude1a|\ud83c\ude2f|\ud83c[\ude32-\ude3a]|\ud83c[\ude50-\ude51]|\u203c|\u2049|[\u25aa-\u25ab]|\u25b6|\u25c0|[\u25fb-\u25fe]|\u00a9|\u00ae|\u2122|\u2139|\ud83c\udc04|[\u2600-\u26FF]|\u2b05|\u2b06|\u2b07|\u2b1b|\u2b1c|\u2b50|\u2b55|\u231a|\u231b|\u2328|\u23cf|[\u23e9-\u23f3]|[\u23f8-\u23fa]|\ud83c\udccf|\u2934|\u2935|[\u2190-\u21ff])/g; + + return body.replace(regex, ''); } openSpacyDialog(comment: Comment): void { - let filteredInputText = this.checkUTFEmoji(this.inputText); + const filteredInputText = this.checkUTFEmoji(this.inputText); this.checkSpellings(filteredInputText).subscribe((res) => { const words: string[] = filteredInputText.trim().split(' '); const errorQuotient = (res.matches.length * 100) / words.length; const hasSpellcheckConfidence = this.checkLanguageConfidence(res); if (hasSpellcheckConfidence && errorQuotient <= 20) { - let commentBodyChecked = filteredInputText; 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: filteredInputText } }); @@ -142,8 +138,6 @@ export class CreateCommentComponent implements OnInit, OnDestroy { }); }; - - /** * Returns a lambda which closes the dialog on call. */ @@ -151,7 +145,6 @@ export class CreateCommentComponent implements OnInit, OnDestroy { return () => this.onNoClick(); } - /** * Returns a lambda which executes the dialog dedicated action on call. */ @@ -170,8 +163,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; } @@ -182,7 +175,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; } 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/_dialogs/worker-dialog/worker-dialog.component.html b/src/app/components/shared/_dialogs/worker-dialog/worker-dialog.component.html new file mode 100644 index 0000000000000000000000000000000000000000..97722af6e6b0e42f4616978b0feac119eb4547d6 --- /dev/null +++ b/src/app/components/shared/_dialogs/worker-dialog/worker-dialog.component.html @@ -0,0 +1,14 @@ +<div id="worker-content"> + <div id="header"> + <button id="btn_hide" (click)="close()">X</button> + <details> + <summary>Laufend # {{getNumberInQueue()}}</summary> + <div mat-dialog-content> + <div *ngFor="let task of taskQueue"> + <span>{{ task.room.name }}, # {{ task.comments.length }}</span> + </div> + </div> + </details> + </div> +</div> + diff --git a/src/app/components/shared/_dialogs/worker-dialog/worker-dialog.component.scss b/src/app/components/shared/_dialogs/worker-dialog/worker-dialog.component.scss new file mode 100644 index 0000000000000000000000000000000000000000..d91e724918261fa302dbf16598697da6a74d641b --- /dev/null +++ b/src/app/components/shared/_dialogs/worker-dialog/worker-dialog.component.scss @@ -0,0 +1,16 @@ +button { + position: absolute !important; + right: 5px; + top: 5px; +} + +::ng-deep .workerContainer > .mat-dialog-container { + padding: 5px !important; + box-shadow: 0 2px 1px -1px rgba(0, 0, 0, .2), 0 1px 1px 0 rgba(0, 0, 0, .14), 0 1px 3px 0 rgba(0, 0, 0, .12), -4px 0 0 0 var(--primary); + overflow: hidden !important; + position: relative !important; +} + +#worker-content { + padding-left: 5px; +} diff --git a/src/app/components/shared/_dialogs/worker-dialog/worker-dialog.component.spec.ts b/src/app/components/shared/_dialogs/worker-dialog/worker-dialog.component.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..650d036a9c0941d90bc163f29cb475c4c543bd63 --- /dev/null +++ b/src/app/components/shared/_dialogs/worker-dialog/worker-dialog.component.spec.ts @@ -0,0 +1,25 @@ +/*import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { WorkerDialogComponent } from './worker-dialog.component'; + +describe('WorkerDialogComponent', () => { + let component: WorkerDialogComponent; + let fixture: ComponentFixture<WorkerDialogComponent>; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ WorkerDialogComponent ] + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(WorkerDialogComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +});*/ 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 new file mode 100644 index 0000000000000000000000000000000000000000..da93bb80e8865f5a714a0ae7a3ce4f5eef4a9b50 --- /dev/null +++ b/src/app/components/shared/_dialogs/worker-dialog/worker-dialog.component.ts @@ -0,0 +1,95 @@ +import { Component, OnInit } from '@angular/core'; +import { Room } from '../../../../models/room'; +import { CommentService } from '../../../../services/http/comment.service'; +import { Comment } from '../../../../models/comment'; +import { SpacyService } from '../../../../services/http/spacy.service'; +import { TSMap } from 'typescript-map'; + +export interface WorkTask { + room: Room; + comments: Comment[]; +} + +@Component({ + selector: 'app-worker-dialog', + templateUrl: './worker-dialog.component.html', + styleUrls: ['./worker-dialog.component.scss'] +}) +export class WorkerDialogComponent implements OnInit { + + isRunning = false; + taskQueue: WorkTask[] = []; + closeCallback: any = null; + + constructor(private commentService: CommentService, + private spacyService: SpacyService) { + } + + ngOnInit(): void { + } + + _callNextInQueue(): void { + if (!this.isQueueEmpty()) { + this.isRunning = true; + const task = this.taskQueue[0]; + this.runWorkTask(task); + } else { + this.isRunning = false; + setTimeout(() => this.close(), 2000); + } + } + + addWorkTask(room: Room): void { + if (this.taskQueue.find((t: WorkTask) => t.room.id === room.id)) { + return; + } + + this.commentService.getAckComments(room.id).subscribe((comments: Comment[]) => { + const task: WorkTask = {room, comments}; + this.taskQueue.push(task); + + if (!this.isRunning) { + this._callNextInQueue(); + } + }); + } + + runWorkTask(task: WorkTask): void { + task.comments.forEach((c: Comment) => { + const model = 'de'; + const text = c.body; + this.spacyService.getKeywords(text, model).subscribe((keywords: string[]) => { + const changes = new TSMap<string, string>(); + changes.set('keywordsFromSpacy', JSON.stringify(keywords)); + this.taskQueue = this.taskQueue.slice(1, this.taskQueue.length); + + this.commentService.patchComment(c, changes).subscribe(_ => { + console.log('PATCHED .........................'); + this._callNextInQueue(); + }, _ => { + this._callNextInQueue(); + }); + }); + }); + } + + getNumberInQueue() { + return this.taskQueue.length; + } + + isQueueEmpty(): boolean { + return this.taskQueue.length === 0; + } + + close(): void { + if (this.closeCallback) { + this.closeCallback(); + } + } + + getCloseCallback(callback: () => void): void { + this.closeCallback = callback; + } + +} + diff --git a/src/app/components/shared/header/header.component.html b/src/app/components/shared/header/header.component.html index af0ed5650534a3f3053fac3340340ccd496edd9e..c72627dae7d5d85aa9cb766bcb3d2dc65d28a9ac 100644 --- a/src/app/components/shared/header/header.component.html +++ b/src/app/components/shared/header/header.component.html @@ -163,6 +163,16 @@ <span>{{'header.tag-cloud' | translate}}</span> </button> + + <button mat-menu-item + tabindex="0" + (click)="startWorkerDialog()"> + <mat-icon>update + </mat-icon> + <span>{{'header.update-spacy-keywords' | translate}}</span> + </button> + + </ng-container> <ng-container *ngIf="router.url.includes('/participant/room/')"> </ng-container> diff --git a/src/app/components/shared/header/header.component.ts b/src/app/components/shared/header/header.component.ts index c547702ec43704f0f588b3ad1708faa0434284a2..9cbf857e9730fbfd44952ced637db8bd0ee8a1f1 100644 --- a/src/app/components/shared/header/header.component.ts +++ b/src/app/components/shared/header/header.component.ts @@ -6,7 +6,7 @@ import { User } from '../../../models/user'; import { UserRole } from '../../../models/user-roles.enum'; import { Location } from '@angular/common'; import { TranslateService } from '@ngx-translate/core'; -import { MatDialog, MatDialogRef } from '@angular/material/dialog'; +import {_MatDialogBase, MAT_DIALOG_DEFAULT_OPTIONS, MatDialog, MatDialogRef} from '@angular/material/dialog'; import { LoginComponent } from '../login/login.component'; import { DeleteAccountComponent } from '../_dialogs/delete-account/delete-account.component'; import { UserService } from '../../../services/http/user.service'; @@ -24,6 +24,7 @@ import { TopicCloudFilterComponent } from '../_dialogs/topic-cloud-filter/topic- import { RoomService } from '../../../services/http/room.service'; import { Room } from '../../../models/room'; import { TagCloudMetaData } from '../../../services/util/tag-cloud-data.service'; +import {WorkerDialogComponent} from "../_dialogs/worker-dialog/worker-dialog.component"; @Component({ selector: 'app-header', @@ -42,6 +43,7 @@ export class HeaderComponent implements OnInit { commentsCountQuestions = 0; commentsCountUsers = 0; commentsCountKeywords = 0; + workerDialogRef: MatDialogRef<WorkerDialogComponent, null> = null; constructor(public location: Location, private authenticationService: AuthenticationService, @@ -322,4 +324,34 @@ export class HeaderComponent implements OnInit { this.roomService.updateRoom(this.room).subscribe(r => this.room = r); } + public startWorkerDialog() { + + if (this.workerDialogRef == null) { + + this.workerDialogRef = this.dialog.open(WorkerDialogComponent, { + width: '200px', + disableClose: true, + autoFocus: false, + position: {left: '50px', bottom: '50px'}, + role: 'dialog', + hasBackdrop: false, + closeOnNavigation: false, + panelClass: 'workerContainer' + }); + + const component: WorkerDialogComponent = this.workerDialogRef.componentInstance; + component.getCloseCallback(() => { + this.workerDialogRef.close(); + this.workerDialogRef = null; + }); + component.addWorkTask(this.room); + } else { + const component: WorkerDialogComponent = this.workerDialogRef.componentInstance; + component.addWorkTask(this.room); + } + + } + + + } diff --git a/src/app/components/shared/shared.module.ts b/src/app/components/shared/shared.module.ts index 9e1fa76e707e4fe232456bbe490910b5f1b96b51..f022dfac582d8515332e41d4450ead18c6e6db9a 100644 --- a/src/app/components/shared/shared.module.ts +++ b/src/app/components/shared/shared.module.ts @@ -37,19 +37,22 @@ import { TopicDialogCommentComponent } from './dialog/topic-dialog-comment/topic import { TopicCloudFilterComponent } from './_dialogs/topic-cloud-filter/topic-cloud-filter.component'; import { SpacyDialogComponent } from './_dialogs/spacy-dialog/spacy-dialog.component'; import { TagCloudPopUpComponent } from './tag-cloud/tag-cloud-pop-up/tag-cloud-pop-up.component'; +import { WorkerDialogComponent } from './_dialogs/worker-dialog/worker-dialog.component'; +import {DragDropModule} from "@angular/cdk/drag-drop"; @NgModule({ - imports: [ - CommonModule, - EssentialsModule, - SharedRoutingModule, - MatRippleModule, - ArsModule, - MarkdownModule, - QRCodeModule, - TagCloudModule, - ColorPickerModule - ], + imports: [ + CommonModule, + EssentialsModule, + SharedRoutingModule, + MatRippleModule, + ArsModule, + MarkdownModule, + QRCodeModule, + TagCloudModule, + ColorPickerModule, + DragDropModule + ], declarations: [ RoomJoinComponent, PageNotFoundComponent, @@ -79,7 +82,8 @@ import { TagCloudPopUpComponent } from './tag-cloud/tag-cloud-pop-up/tag-cloud-p TopicDialogCommentComponent, TopicCloudFilterComponent, SpacyDialogComponent, - TagCloudPopUpComponent + TagCloudPopUpComponent, + WorkerDialogComponent ], exports: [ RoomJoinComponent, 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 65e367adb959663a8e22fe6966624e26368b9c44..2fb3006994edebc06e0231cb87e40867b338c128 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,39 +1,70 @@ <div #popupContainer class="popupContainer" - (focusout)="onFocus($event)" - tabindex="0"> - <span> - <mat-icon matTooltip="{{'tag-cloud.overview-question-topic-tooltip' | translate}}">comment</mat-icon> - <p> - {{tagData && tagData.comments.length}} - </p> - <mat-icon matTooltip="{{'tag-cloud.overview-questioners-topic-tooltip' | translate}}">person</mat-icon> - <p> - {{tagData && tagData.distinctUsers.size}} - </p> - <mat-icon matTooltip="{{'tag-cloud.upvote-topic' | translate}}">thumb_up</mat-icon> - <p> - {{tagData && tagData.cachedUpVotes}} - </p> - <mat-icon matTooltip="{{'tag-cloud.downvote-topic' | translate}}">thumb_down</mat-icon> - <p> - {{tagData && tagData.cachedDownVotes}} - </p> - <button *ngIf="user && user.role >= 1" mat-button (click)="addBlacklistWord()"> - <mat-icon matTooltip="{{'tag-cloud.blacklist-topic' | translate}}">gavel</mat-icon> - </button> - </span> - <br> - <span> - <mat-icon matTooltip="{{'tag-cloud.period-since-first-comment' | translate}}">date_range</mat-icon> - <p> - {{timePeriodText}} - </p> - </span> - <div *ngIf="categories && categories.length"> - <p>Kategorien:</p> - <ul> - <li *ngFor="let category of categories">{{category}}</li> - </ul> + (focusout)="onFocusOut()" + tabindex="0"> + <div> + <div> + <span> + <mat-icon matTooltip="{{'tag-cloud.overview-question-topic-tooltip' | translate}}">comment</mat-icon> + <p> + {{tagData && tagData.comments.length}} + </p> + </span> + <span> + <mat-icon matTooltip="{{'tag-cloud.overview-questioners-topic-tooltip' | translate}}">person</mat-icon> + <p> + {{tagData && tagData.distinctUsers.size}} + </p> + </span> + <button *ngIf="user && user.role >= 1" mat-button (click)="addBlacklistWord()"> + <mat-icon matTooltip="{{'tag-cloud.blacklist-topic' | translate}}">gavel</mat-icon> + </button> + </div> + <div> + <span> + <mat-icon matTooltip="{{'tag-cloud.upvote-topic' | translate}}">thumb_up</mat-icon> + <p> + {{tagData && tagData.cachedUpVotes}} + </p> + </span> + <span> + <mat-icon matTooltip="{{'tag-cloud.downvote-topic' | translate}}">thumb_down</mat-icon> + <p> + {{tagData && tagData.cachedDownVotes}} + </p> + </span> + </div> + <div> + <span> + <mat-icon matTooltip="{{'tag-cloud.period-since-first-comment' | translate}}">date_range</mat-icon> + <p> + {{timePeriodText}} + </p> + </span> + </div> + <div class="replacementContainer" *ngIf="checkLanguage"> + <mat-form-field> + <mat-label>{{'tag-cloud-popup.tag-correction-placeholder' | translate}}</mat-label> + <input type="text" + aria-label="{{'tag-cloud-popup.tag-correction-placeholder' | translate}}" + matInput + (keyup)="checkEnter($event)" + [formControl]="replacementInput" + [matAutocomplete]="auto"> + <mat-autocomplete #auto="matAutocomplete"> + <mat-option *ngFor="let data of spellingData" [value]="data"> + {{data}} + </mat-option> + </mat-autocomplete> + </mat-form-field> + <br> + <button mat-button class="replace-button" (click)="updateTag()">{{"comment-page.send" | translate}}</button> + </div> + <div *ngIf="categories && categories.length"> + <p>Kategorien:</p> + <ul> + <li *ngFor="let category of categories">{{category}}</li> + </ul> + </div> </div> </div> diff --git a/src/app/components/shared/tag-cloud/tag-cloud-pop-up/tag-cloud-pop-up.component.scss b/src/app/components/shared/tag-cloud/tag-cloud-pop-up/tag-cloud-pop-up.component.scss index 057e27b57a667a064649da4332cadcb7b5a40733..9ac82a2670707fee059ec00fc7e4ff7941f11b39 100644 --- a/src/app/components/shared/tag-cloud/tag-cloud-pop-up/tag-cloud-pop-up.component.scss +++ b/src/app/components/shared/tag-cloud/tag-cloud-pop-up/tag-cloud-pop-up.component.scss @@ -16,6 +16,18 @@ $header-size: 67px; z-index: 3; color: var(--on-dialog); transform: translateY(-$header-size); + min-width: 200px; + display: flex; + justify-content: center; + align-items: center; + + & > div { + display: flex; + justify-content: center; + overflow: auto; + flex-direction: column; + align-items: flex-start; + } &:focus { outline: none; @@ -116,3 +128,48 @@ button { padding-left: 10px; padding-right: 10px; } + +.replace-button { + background-color: var(--primary); + color: var(--on-primary); + min-width: 30px !important; + min-height: 10px !important; + width: 100%; + z-index: 3; +} + +::ng-deep .replacementContainer { + align-self: center; + + .mat-form-field-infix { + min-width: 150px; + width: min-content; + } + + .mat-form-field-wrapper { + margin-bottom: -0.8em; + } +} + +mat-form-field { + border-radius: 5px; + line-height: 80%; + caret-color: var(--on-surface); + -webkit-appearance: textarea; + min-height: 30px; + min-width: 140px; + cursor: text; + color: var(--on-dialog); + background-color: var(--dialog); + margin-top: 0.2em; + + &:focus { + outline: none; + } +} + +#replacement { + color: var(--on-dialog); + background-color: var(--dialog); + border-radius: 5px; +} 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 2b3210c4330c47da7851821ffbf8b9e268d176f0..c4fed49e87dcc9ad06cfcf33b36a6378bcfd9316 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 @@ -1,10 +1,15 @@ -import { AfterViewInit, Component, ElementRef, Input, OnInit, ViewChild } from '@angular/core'; +import { AfterViewInit, Component, ElementRef, OnInit, ViewChild } from '@angular/core'; import { TranslateService } from '@ngx-translate/core'; import { LanguageService } from '../../../../services/util/language.service'; -import { TagCloudComponent } from '../tag-cloud.component'; import { AuthenticationService } from '../../../../services/http/authentication.service'; import { User } from '../../../../models/user'; -import { TagCloudDataTagEntry } from '../../../../services/util/tag-cloud-data.service'; +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 { NotificationService } from '../../../../services/util/notification.service'; +import { MatAutocompleteTrigger } from '@angular/material/autocomplete'; const CLOSE_TIME = 1500; @@ -15,19 +20,28 @@ const CLOSE_TIME = 1500; }) export class TagCloudPopUpComponent implements OnInit, AfterViewInit { - @Input() parent: TagCloudComponent; @ViewChild('popupContainer') popupContainer: ElementRef; + @ViewChild(MatAutocompleteTrigger) trigger: MatAutocompleteTrigger; + replacementInput = new FormControl(); tag: string; tagData: TagCloudDataTagEntry; categories: string[]; timePeriodText: string; user: User; + selectedLang: Language = 'en-US'; + spellingData: string[] = []; + checkLanguage = false; private _popupHoverTimer: number; private _popupCloseTimer: number; + private _hasLeft = true; constructor(private langService: LanguageService, private translateService: TranslateService, - private authenticationService: AuthenticationService) { + private authenticationService: AuthenticationService, + private tagCloudDataService: TagCloudDataService, + private languagetoolService: LanguagetoolService, + private commentService: CommentService, + private notificationService: NotificationService) { this.langService.langEmitter.subscribe(lang => { this.translateService.use(lang); }); @@ -46,30 +60,40 @@ export class TagCloudPopUpComponent implements OnInit, AfterViewInit { const html = this.popupContainer.nativeElement as HTMLDivElement; html.addEventListener('mouseenter', () => { clearTimeout(this._popupCloseTimer); + this._hasLeft = false; }); html.addEventListener('mouseleave', () => { - this._popupCloseTimer = setTimeout(() => { - this.close(); - }, CLOSE_TIME); + this._hasLeft = true; + this.close(); }); } - onFocus(event) { - if (!this.popupContainer.nativeElement.contains(event.target)) { - this.close(); - } + onFocusOut() { + this.close(); } leave(): void { clearTimeout(this._popupHoverTimer); - this._popupCloseTimer = setTimeout(() => { - this.close(); - }, CLOSE_TIME); + this.close(); } - enter(elem: HTMLElement, tag: string, tagData: TagCloudDataTagEntry, hoverDelayInMs: number): void { + enter(elem: HTMLElement, tag: string, tagData: TagCloudDataTagEntry, hoverDelayInMs: number, checkLanguage: boolean): void { + this.checkLanguage = checkLanguage; + if (checkLanguage) { + this.spellingData = []; + this.languagetoolService.checkSpellings(tag, this.selectedLang).subscribe(correction => { + 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); + } + } + } + }); + } clearTimeout(this._popupCloseTimer); clearTimeout(this._popupHoverTimer); + this._hasLeft = true; this._popupHoverTimer = setTimeout(() => { this.tag = tag; this.tagData = tagData; @@ -81,13 +105,63 @@ export class TagCloudPopUpComponent implements OnInit, AfterViewInit { } addBlacklistWord(): void { - this.parent.dataManager.blockWord(this.tag); - this.close(); + this.tagCloudDataService.blockWord(this.tag); + 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 (!this._hasLeft || (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.translateService.get('topic-cloud-dialog.keyword-edit').subscribe(msg => { + this.notificationService.show(msg); + }); + }, _ => { + this.translateService.get('topic-cloud-dialog.changes-gone-wrong').subscribe(msg => { + this.notificationService.show(msg); + }); + }); + }); + this.close(false); + this.replacementInput.reset(); + this.trigger.closePanel(); + } + + checkEnter(e: KeyboardEvent) { + if (e.key === 'Enter') { + this.updateTag(); + } } private position(elem: HTMLElement) { diff --git a/src/app/components/shared/tag-cloud/tag-cloud.component.html b/src/app/components/shared/tag-cloud/tag-cloud.component.html index 6f6afd01783495837b2d1ac015d6a7644755fc2c..ece66e1f97ae2378b904bfac66a9605ab499c5d7 100644 --- a/src/app/components/shared/tag-cloud/tag-cloud.component.html +++ b/src/app/components/shared/tag-cloud/tag-cloud.component.html @@ -1,11 +1,11 @@ <ars-screen ars-flex-box> <mat-drawer-container class="spacyTagCloudContainer"> - <mat-drawer [(opened)]="configurationOpen" position="end" mode="push"> + <mat-drawer [(opened)]="configurationOpen" position="end" mode="over"> <app-cloud-configuration [parent]="this"></app-cloud-configuration> </mat-drawer> <mat-drawer-content> <ars-fill ars-flex-box> - <app-tag-cloud-pop-up [parent]="this"></app-tag-cloud-pop-up> + <app-tag-cloud-pop-up></app-tag-cloud-pop-up> <div [ngClass]="{'hidden': !isLoading}" fxLayout="row" fxLayoutAlign="center center" fxFill> <mat-progress-spinner *ngIf="isLoading" mode="indeterminate"></mat-progress-spinner> </div> diff --git a/src/app/components/shared/tag-cloud/tag-cloud.component.ts b/src/app/components/shared/tag-cloud/tag-cloud.component.ts index 451e5bb905137f85500b9aa7824f08e4aef690dc..b0d32a0a21568814681165aff06b8dc1dd105f2a 100644 --- a/src/app/components/shared/tag-cloud/tag-cloud.component.ts +++ b/src/app/components/shared/tag-cloud/tag-cloud.component.ts @@ -110,6 +110,10 @@ const getDefaultCloudParameters = (): CloudParameters => { {maxVisibleElements: -1, color: resDefaultColors[10], rotation: 0}, ]; return { + fontFamily: 'Helvetica,Arial,sans-serif', + fontWeight: 'normal', + fontStyle: 'normal', + fontSize: '10px', backgroundColor: resDefaultColors[11], fontColor: resDefaultColors[0], fontSizeMin: 100, @@ -121,7 +125,7 @@ const getDefaultCloudParameters = (): CloudParameters => { randomAngles: false, checkSpelling: true, sortAlphabetically: false, - textTransform: CloudTextStyle.lowercase, + textTransform: CloudTextStyle.normal, cloudWeightSettings: weightSettings }; }; @@ -198,7 +202,7 @@ export class TagCloudComponent implements OnInit, AfterViewInit, OnDestroy { const temp: CloudParameters = jsonData != null ? JSON.parse(jsonData) : null; const elem = getDefaultCloudParameters(); if (temp != null) { - for (const key in Object.keys(elem)) { + for (const key of Object.keys(elem)) { if (temp[key] !== undefined) { elem[key] = temp[key]; } @@ -310,7 +314,6 @@ export class TagCloudComponent implements OnInit, AfterViewInit, OnDestroy { this.setCloudParameters(getDefaultCloudParameters()); } - onResize(event: UIEvent): any { this.updateTagCloud(); } @@ -375,7 +378,7 @@ export class TagCloudComponent implements OnInit, AfterViewInit, OnDestroy { } openTags(tag: CloudData): void { - if (this._subscriptionCommentlist !== null) { + if (this.dataManager.demoActive || this._subscriptionCommentlist !== null) { return; } this._subscriptionCommentlist = this.eventService.on('commentListCreated').subscribe(() => { @@ -421,7 +424,8 @@ export class TagCloudComponent implements OnInit, AfterViewInit, OnDestroy { }); elem.addEventListener('mouseenter', () => { this.popup.enter(elem, dataElement.text, dataElement.tagData, - (this._currentSettings.hoverTime + this._currentSettings.hoverDelay) * 1_000); + (this._currentSettings.hoverTime + this._currentSettings.hoverDelay) * 1_000, + this._currentSettings.checkSpelling); }); }); } @@ -437,25 +441,33 @@ export class TagCloudComponent implements OnInit, AfterViewInit, OnDestroy { for (let i = rules.length - 1; i >= 0; i--) { customTagCloudStyles.sheet.deleteRule(i); } + // global let textTransform = ''; if (this._currentSettings.textTransform === CloudTextStyle.capitalized) { - textTransform = 'text-transform: uppercase;'; + textTransform = 'text-transform: capitalize;'; } else if (this._currentSettings.textTransform === CloudTextStyle.lowercase) { textTransform = 'text-transform: lowercase;'; + } else if (this._currentSettings.textTransform === CloudTextStyle.uppercase) { + textTransform = 'text-transform: uppercase;'; } + customTagCloudStyles.sheet.insertRule('.spacyTagCloud > span, .spacyTagCloud > span > a { ' + + textTransform + ' font-family: ' + this._currentSettings.fontFamily + '; ' + + 'font-size: ' + this._currentSettings.fontSize + '; ' + + 'font-weight: ' + this._currentSettings.fontWeight + '; ' + + 'font-style' + this._currentSettings.fontStyle + '; }'); + // custom spans const fontRange = (this._currentSettings.fontSizeMax - this._currentSettings.fontSizeMin) / 10; for (let i = 1; i <= 10; i++) { - customTagCloudStyles.sheet.insertRule('.spacyTagCloud > span.w' + i + - ', .spacyTagCloud > span.w' + i + ' > a { ' - + 'color: ' + this._currentSettings.cloudWeightSettings[i - 1].color + ';' + - textTransform + ' font-size: ' + - (this._currentSettings.fontSizeMin + fontRange * i).toFixed(0) + '%; }', rules.length); + customTagCloudStyles.sheet.insertRule('.spacyTagCloud > span.w' + i + ', ' + + '.spacyTagCloud > span.w' + i + ' > a { ' + + 'color: ' + this._currentSettings.cloudWeightSettings[i - 1].color + '; ' + + 'font-size: ' + (this._currentSettings.fontSizeMin + fontRange * i).toFixed(0) + '%; }'); } - customTagCloudStyles.sheet.insertRule('.spacyTagCloud > span:hover, .spacyTagCloud > span:hover > a { color: ' + - this._currentSettings.fontColor + '; background-color: ' + - this._currentSettings.backgroundColor + '; }', rules.length); - customTagCloudStyles.sheet.insertRule('.spacyTagCloudContainer { background-color: ' + - this._currentSettings.backgroundColor + '; }', rules.length); + customTagCloudStyles.sheet.insertRule('.spacyTagCloud > span:hover, .spacyTagCloud > span:hover > a { ' + + 'color: ' + this._currentSettings.fontColor + '; ' + + 'background-color: ' + this._currentSettings.backgroundColor + '; }'); + customTagCloudStyles.sheet.insertRule('.spacyTagCloudContainer { ' + + 'background-color: ' + this._currentSettings.backgroundColor + '; }'); } /** @@ -470,8 +482,9 @@ export class TagCloudComponent implements OnInit, AfterViewInit, OnDestroy { /* hoverScale, hoverTime, hoverDelay, delayWord can be updated without refreshing */ - const cssUpdates = ['backgroundColor', 'fontColor', 'fontSizeMin', 'fontSizeMax', 'textTransform']; - const dataUpdates = ['randomAngles', 'sortAlphabetically', 'checkSpelling']; + const cssUpdates = ['backgroundColor', 'fontColor']; + const dataUpdates = ['randomAngles', 'sortAlphabetically', + 'fontSizeMin', 'fontSizeMax', 'textTransform', 'fontStyle', 'fontWeight', 'fontFamily', 'fontSize']; const cssWeightUpdates = ['color']; const dataWeightUpdates = ['maxVisibleElements', 'rotation']; //data updates diff --git a/src/app/components/shared/tag-cloud/tag-cloud.interface.ts b/src/app/components/shared/tag-cloud/tag-cloud.interface.ts index ca84784ddeec0566a7f302f303831c87aa7c5415..b6f0edf187bf9b80fc536327d88e54c876e8b7c5 100644 --- a/src/app/components/shared/tag-cloud/tag-cloud.interface.ts +++ b/src/app/components/shared/tag-cloud/tag-cloud.interface.ts @@ -33,10 +33,27 @@ export type CloudWeightSettings = [ export enum CloudTextStyle { normal, lowercase, - capitalized + capitalized, + uppercase } export interface CloudParameters { + /** + * The general font family for the tag cloud + */ + fontFamily: string; + /** + * The general font style for the tag cloud + */ + fontStyle: string; + /** + * The general font weight for the tag cloud + */ + fontWeight: string; + /** + * The general font size for the tag cloud + */ + fontSize: string; /** * Background color of the Tag-cloud */ diff --git a/src/app/services/http/spacy.service.ts b/src/app/services/http/spacy.service.ts index 92825bb8dc2bb79c17bd494feaedab11be444a4a..394000f9c7c237963f8200ee39dce98671221c43 100644 --- a/src/app/services/http/spacy.service.ts +++ b/src/app/services/http/spacy.service.ts @@ -2,65 +2,29 @@ import { Injectable } from '@angular/core'; import { HttpClient, HttpHeaders } from '@angular/common/http'; import { Observable } from 'rxjs'; import { BaseHttpService } from './base-http.service'; -import { catchError, map } from 'rxjs/operators'; +import { catchError, map, tap } from 'rxjs/operators'; -export type Model = 'de' | 'en' | 'fr'; +export type Model = 'de' | 'en' | 'fr' | 'es' | 'it' | 'nl' | 'pt'; -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 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; - } +//[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'}) }; @@ -73,17 +37,44 @@ export class SpacyService extends BaseHttpService { super(); } - getKeywords(text: string, model: string): Observable<string[]> { - return this.analyse(text, model).pipe( - map(result => result.words.filter(v => v.tag.charAt(0) === 'N').map(v => v.text)) - ); + 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; + } + } + }); + 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; + }) ); } } diff --git a/src/app/services/util/tag-cloud-data.service.ts b/src/app/services/util/tag-cloud-data.service.ts index 48e81f9512fb43f10f050551c026c574dc538509..8d9ec90e30d238b75c59974a951da943d12e11aa 100644 --- a/src/app/services/util/tag-cloud-data.service.ts +++ b/src/app/services/util/tag-cloud-data.service.ts @@ -81,9 +81,7 @@ export class TagCloudDataService { private readonly _currentMetaData: TagCloudMetaData; private _demoData: TagCloudData = null; private _adminData: TopicCloudAdminData = null; - private _currentBlacklist: string[] = []; private _subscriptionAdminData: Subscription; - private _subscriptionBlacklist: Subscription; constructor(private _wsCommentService: WsCommentServiceService, private _commentService: CommentService, @@ -109,16 +107,11 @@ export class TagCloudDataService { bindToRoom(roomId: string): void { this._roomId = roomId; + this.onReceiveAdminData(this._tagCloudAdmin.getDefaultAdminData); this._subscriptionAdminData = this._tagCloudAdmin.getAdminData.subscribe(adminData => { - this._adminData = adminData; - this._calcWeightType = this._adminData.considerVotes ? TagCloudCalcWeightType.byLengthAndVotes : TagCloudCalcWeightType.byLength; - this._supplyType = this._adminData.keywordORfulltext as unknown as TagCloudDataSupplyType; - this.rebuildTagData(); - }); - this._subscriptionBlacklist = this._tagCloudAdmin.getBlacklist().subscribe(blacklist => { - this._currentBlacklist = blacklist || []; - this.rebuildTagData(); + this.onReceiveAdminData(adminData,true); }); + this.fetchData(); if (!CommentFilterOptions.readFilter().paused) { this._wsCommentSubscription = this._wsCommentService @@ -127,7 +120,6 @@ export class TagCloudDataService { } unbindRoom(): void { - this._subscriptionBlacklist.unsubscribe(); this._subscriptionAdminData.unsubscribe(); if (this._wsCommentSubscription !== null) { this._wsCommentSubscription.unsubscribe(); @@ -252,6 +244,15 @@ export class TagCloudDataService { this._dataBus.next(newData); } + private onReceiveAdminData(data: TopicCloudAdminData, update = false) { + this._adminData = data; + this._calcWeightType = this._adminData.considerVotes ? TagCloudCalcWeightType.byLengthAndVotes : TagCloudCalcWeightType.byLength; + this._supplyType = this._adminData.keywordORfulltext as unknown as TagCloudDataSupplyType; + if(update) { + this.rebuildTagData(); + } + } + private getCurrentData(): TagCloudData { if (this._isDemoActive) { return this._demoData; @@ -304,7 +305,7 @@ export class TagCloudDataService { for (const keyword of keywords) { const lowerCaseKeyWord = keyword.toLowerCase(); let profanity = false; - for (const word of this._currentBlacklist) { + for (const word of this._adminData.blacklist) { if (lowerCaseKeyWord.includes(word)) { profanity = true; break; @@ -399,6 +400,14 @@ export class TagCloudDataService { comment.downvotes = value as number; needRebuild = true; break; + case 'keywordsFromSpacy': + comment.keywordsFromSpacy = JSON.parse(value as string); + needRebuild = true; + break; + case 'keywordsFromQuestioner': + comment.keywordsFromQuestioner = JSON.parse(value as string); + needRebuild = true; + break; case 'ack': const isNowAck = value as boolean; if (!isNowAck) { diff --git a/src/assets/i18n/home/de.json b/src/assets/i18n/home/de.json index e54b40b66a45a4530e80e9aa144bb5ef949697aa..33ec19d02e982e9a88488ed9071dae8c0feea975 100644 --- a/src/assets/i18n/home/de.json +++ b/src/assets/i18n/home/de.json @@ -100,7 +100,8 @@ "questions-blocked": "Fragen sind deaktiviert!", "overview-question-tooltip": "Anzahl gestellter Fragen", "overview-questioners-tooltip": "Anzahl Fragensteller*innen", - "overview-keywords-tooltip": "Anzahl Schlüsselwörter" + "overview-keywords-tooltip": "Anzahl Schlüsselwörter", + "update-spacy-keywords": "Stichwörter anlegen" }, "help": { "cancel": "Schließen", diff --git a/src/assets/i18n/home/en.json b/src/assets/i18n/home/en.json index b886992ec32e98181f3c3a17c4d455cb38749ba4..2c6b9e93e1f6a502e9946db05131bf9059fb761e 100644 --- a/src/assets/i18n/home/en.json +++ b/src/assets/i18n/home/en.json @@ -92,7 +92,8 @@ "questions-blocked": "No further questions!", "overview-question-tooltip": "Number of questions", "overview-questioners-tooltip": "Number of questioners", - "overview-keywords-tooltip": "Number of Keywords" + "overview-keywords-tooltip": "Number of Keywords", + "update-spacy-keywords": "Add keywords" }, "help": { "cancel": "Close", diff --git a/src/assets/i18n/participant/de.json b/src/assets/i18n/participant/de.json index baee4ae21a3d403ad978fbfa9e0ab322966b7986..800272a2057b95ef362e5f269befd79433b5f8b1 100644 --- a/src/assets/i18n/participant/de.json +++ b/src/assets/i18n/participant/de.json @@ -251,7 +251,8 @@ "some-days": "{{days}} Tage", "one-week": "1 Woche", "some-weeks": "{{weeks}} Wochen", - "some-months": "{{months}} Monate" + "some-months": "{{months}} Monate", + "tag-correction-placeholder": "Korrektur" }, "topic-cloud-dialog": { "cancel": "Abbrechen", @@ -354,4 +355,4 @@ "rotate-weight": "Einige Einträge dieser Klasse zufällig um x Grad drehen", "rotate-weight-tooltip": "einige Einträge dieser Wichtigkeitsklasse zufällig um x Grad drehen" } -} \ No newline at end of file +} diff --git a/src/assets/i18n/participant/en.json b/src/assets/i18n/participant/en.json index da69a91b4d84e963b43e8fddf3245a0d70bb2305..218f91314adbdc87d0ecca09914245e3bc4251f3 100644 --- a/src/assets/i18n/participant/en.json +++ b/src/assets/i18n/participant/en.json @@ -257,7 +257,8 @@ "some-days": "{{days}} days", "one-week": "1 week", "some-weeks": "{{weeks}} weeks", - "some-months": "{{months}} months" + "some-months": "{{months}} months", + "tag-correction-placeholder": "Correction" }, "topic-cloud-dialog":{ "edit": "Edit", @@ -360,4 +361,4 @@ "highestWeight-tooltip": "show x tags with the highest weight", "rotate-weight": "Rotate some entries of this weight class randomly by x degrees" } -} \ No newline at end of file +}