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 6d2293ef26933cb45f39916938efdf56e5fa54ba..ae82095d8dc651122c10971f5caa690691a23002 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,4 +1,4 @@ -import { Component, Inject, OnInit, ViewChild } from '@angular/core'; +import { Component, Inject, Input, OnInit, ViewChild } from '@angular/core'; import { Comment, Language as CommentLanguage } from '../../../../models/comment'; import { NotificationService } from '../../../../services/util/notification.service'; import { MAT_DIALOG_DATA, MatDialog, MatDialogRef } from '@angular/material/dialog'; @@ -9,8 +9,6 @@ import { SpacyDialogComponent } from '../spacy-dialog/spacy-dialog.component'; import { LanguagetoolService, Language } from '../../../../services/http/languagetool.service'; import { CreateCommentKeywords } from '../../../../utils/create-comment-keywords'; import { WriteCommentComponent } from '../../write-comment/write-comment.component'; -import { DeepLDialogComponent } from '../deep-ldialog/deep-ldialog.component'; -import { DeepLService } from '../../../../services/http/deep-l.service'; @Component({ selector: 'app-submit-comment', @@ -20,10 +18,11 @@ import { DeepLService } from '../../../../services/http/deep-l.service'; export class CreateCommentComponent implements OnInit { @ViewChild(WriteCommentComponent) commentComponent: WriteCommentComponent; - user: User; - roomId: string; - tags: string[]; + @Input() user: User; + @Input() roomId: string; + @Input() tags: string[]; isSendingToSpacy = false; + isModerator = false; constructor( private notification: NotificationService, @@ -32,12 +31,12 @@ export class CreateCommentComponent implements OnInit { public dialog: MatDialog, public languagetoolService: LanguagetoolService, public eventService: EventService, - private deeplService: DeepLService, @Inject(MAT_DIALOG_DATA) public data: any) { } ngOnInit() { this.translateService.use(localStorage.getItem('currentLang')); + this.isModerator = this.user && this.user.role > 0; } onNoClick(): void { @@ -52,32 +51,7 @@ export class CreateCommentComponent implements OnInit { comment.createdFromLecturer = this.user.role > 0; comment.tag = tag; this.isSendingToSpacy = true; - this.openDeeplDialog(body, text, (newBody: string, newText: string) => { - comment.body = newBody; - this.openSpacyDialog(comment, newText); - }); - } - - openDeeplDialog(body: string, text: string, onClose: (data: string, text: string) => void) { - this.deeplService.improveTextStyle(text).subscribe(improvedText => { - this.isSendingToSpacy = false; - this.dialog.open(DeepLDialogComponent, { - width: '900px', - maxWidth: '100%', - data: { - body, - text, - improvedText - } - }).afterClosed().subscribe((res) => { - if (res) { - onClose(res.body, res.text); - } - }); - }, (_) => { - this.isSendingToSpacy = false; - onClose(body, text); - }); + this.openSpacyDialog(comment, text); } openSpacyDialog(comment: Comment, rawText: string): void { diff --git a/src/app/components/shared/_dialogs/deep-ldialog/deep-ldialog.component.html b/src/app/components/shared/_dialogs/deep-ldialog/deep-ldialog.component.html index 4ca1fdca87ce3e2cfc4dec13215e7281015b54a4..a2489068f100c7f1c4db88f3aa763b73658debe6 100644 --- a/src/app/components/shared/_dialogs/deep-ldialog/deep-ldialog.component.html +++ b/src/app/components/shared/_dialogs/deep-ldialog/deep-ldialog.component.html @@ -7,19 +7,47 @@ [(ngModel)]="radioButtonValue"> <mat-radio-button [value]="normalValue"> <small>{{'deepl.option-normal' | translate}}</small> - <app-view-comment-data [currentData]="normalValue.body"></app-view-comment-data> </mat-radio-button> + <app-view-comment-data #normal + [maxTextCharacters]="data.maxTextCharacters" + [maxDataCharacters]="data.maxDataCharacters" + [isModerator]="data.isModerator" + [isEditor]="true" + [currentData]="normalValue.body"></app-view-comment-data> <br> <mat-radio-button [value]="improvedValue"> <small>{{'deepl.option-improved' | translate}}</small> - <app-view-comment-data [currentData]="improvedValue.body"></app-view-comment-data> </mat-radio-button> + <app-view-comment-data #improved + [maxTextCharacters]="data.maxTextCharacters" + [maxDataCharacters]="data.maxDataCharacters" + [isModerator]="data.isModerator" + [isEditor]="true" + [currentData]="improvedValue.body" + [usesFormality]="true" + [formalityEmitter]="onFormalityChange.bind(this)" + [selectedFormality]="'less'"></app-view-comment-data> </mat-radio-group> </div> -<app-dialog-action-buttons - [buttonsLabelSection]="'comment-page'" - [confirmButtonLabel]="'continue'" - [cancelButtonClickAction]="buildCloseDialogActionCallback()" - [confirmButtonClickAction]="buildSubmitBodyActionCallback()"> -</app-dialog-action-buttons> +<ars-row ars-flex-box class="action-button-container"> + <ars-col> + <button + mat-flat-button + class="help-button" + (click)="openHelp()"> + <mat-icon>help</mat-icon> + {{ 'explanation.label' | translate}} + </button> + </ars-col> + <ars-col> + <app-dialog-action-buttons + [buttonsLabelSection]="'comment-page'" + [confirmButtonLabel]="'continue'" + [showDivider]="false" + [spacing]="false" + [cancelButtonClickAction]="buildCloseDialogActionCallback()" + [confirmButtonClickAction]="buildSubmitBodyActionCallback()"> + </app-dialog-action-buttons> + </ars-col> +</ars-row> diff --git a/src/app/components/shared/_dialogs/deep-ldialog/deep-ldialog.component.scss b/src/app/components/shared/_dialogs/deep-ldialog/deep-ldialog.component.scss index a667f841e767403a0ef2706b9ec756474a0afdde..f9db8f1d8374e2d4e94e011909a0014cbad4aee9 100644 --- a/src/app/components/shared/_dialogs/deep-ldialog/deep-ldialog.component.scss +++ b/src/app/components/shared/_dialogs/deep-ldialog/deep-ldialog.component.scss @@ -1,5 +1,4 @@ ::ng-deep { - mat-radio-button { width: 100%; } @@ -11,10 +10,26 @@ .mat-radio-label-content { width: 100%; } +} + +.action-button-container { + @media screen and (max-width: 500px) { + overflow: auto; + display: flex; + justify-content: space-between; + flex-direction: column !important; + flex-wrap: wrap; + align-items: flex-end; + } +} + +.help-button { + background-color: var(--secondary); + color: var(--on-secondary); + margin-top: 1rem; - app-view-comment-data > div { - background: var(--surface); - border-radius: 10px; - padding: 7px; + .mat-icon { + font-size: 18px; + margin-top: 3px; } } 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 9a835d9ec84cb57fee7ac368541910baf775652b..df77ec4f0b719af90b159d2a4eef17c2e9dfcc40 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 @@ -1,10 +1,19 @@ -import { Component, Inject, OnInit } from '@angular/core'; -import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; +import { AfterViewInit, Component, Inject, OnInit, ViewChild } from '@angular/core'; +import { MAT_DIALOG_DATA, MatDialog, MatDialogRef } from '@angular/material/dialog'; import { ViewCommentDataComponent } from '../../view-comment-data/view-comment-data.component'; +import { NotificationService } from '../../../../services/util/notification.service'; +import { LanguageService } from '../../../../services/util/language.service'; +import { TranslateService } from '@ngx-translate/core'; +import { WriteCommentComponent } from '../../write-comment/write-comment.component'; +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'; interface ResultValue { body: string; text: string; + view: ViewCommentDataComponent; } @Component({ @@ -12,79 +21,133 @@ interface ResultValue { templateUrl: './deep-ldialog.component.html', styleUrls: ['./deep-ldialog.component.scss'] }) -export class DeepLDialogComponent implements OnInit { +export class DeepLDialogComponent implements OnInit, AfterViewInit { + @ViewChild('normal') normal: ViewCommentDataComponent; + @ViewChild('improved') improved: ViewCommentDataComponent; radioButtonValue: ResultValue; normalValue: ResultValue; improvedValue: ResultValue; + supportsFormality: boolean; constructor( private dialogRef: MatDialogRef<DeepLDialogComponent>, - @Inject(MAT_DIALOG_DATA) public data: any) { + @Inject(MAT_DIALOG_DATA) public data: any, + private notificationService: NotificationService, + private languageService: LanguageService, + private translateService: TranslateService, + private deeplService: DeepLService, + private dialog: MatDialog) { + this.languageService.langEmitter.subscribe(lang => { + this.translateService.use(lang); + }); + 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(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 = { body: this.data.body, - text: this.data.text + text: this.data.text, + view: this.normal }; - const sentences = this.data.improvedText.split('\n').filter(sent => sent.length > 0); - const delta = ViewCommentDataComponent.getDeltaFromData(this.data.body); - if (delta === null) { - setTimeout(() => this.dialogRef.close(this.normalValue)); - return; - } - const ops = delta.ops; - let i = 0; - let sentenceIndex = 0; - let lastFoundIndex = -1; - for (; i < ops.length && sentenceIndex < sentences.length; i++) { - const data = ops[i]['insert']; - if (typeof data !== 'string') { - continue; - } - if (data === '\n') { - continue; - } - const endsNewline = data.endsWith('\n'); - const mod = (endsNewline ? -1 : 0) + (data.startsWith('\n') ? -1 : 0); - const occurrence = data.split('\n').length + mod; - ops[i]['insert'] = sentences.slice(sentenceIndex, sentenceIndex + occurrence).join('\n') + - (endsNewline ? '\n' : ''); - sentenceIndex += occurrence; - lastFoundIndex = i; - } - for (let j = ops.length - 1; j >= i; j--) { - const data = ops[i]['insert']; - if (data === 'string' && data.trim().length) { - ops.splice(j, 1); - } - } - if (sentenceIndex < sentences.length) { - if (lastFoundIndex < 0) { - setTimeout(() => this.dialogRef.close(this.normalValue)); - return; - } - let data = ops[i]['insert']; - const endsNewline = data.endsWith('\n'); - if (endsNewline) { - data = data.substring(0, data.length - 1); - } - ops[i]['insert'] = data + sentences.slice(sentenceIndex).join('\n') + (endsNewline ? '\n' : ''); - } this.improvedValue = { - body: ViewCommentDataComponent.getDataFromDelta(delta), - text: this.data.improvedText + body: this.data.improvedBody, + text: this.data.improvedText, + view: this.improved }; this.radioButtonValue = this.normalValue; } + ngAfterViewInit() { + this.normal.afterEditorInit = () => { + this.normal.buildMarks(this.data.text, this.data.result); + }; + } + buildCloseDialogActionCallback(): () => void { return () => this.dialogRef.close(); } buildSubmitBodyActionCallback(): () => void { - return () => this.dialogRef.close(this.radioButtonValue); + return () => { + let current: ResultValue; + if (this.radioButtonValue === this.normalValue) { + this.normalValue.body = this.normal.currentData; + this.normalValue.text = this.normal.currentText; + this.normalValue.view = this.normal; + current = this.normalValue; + } else { + this.improvedValue.body = this.improved.currentData; + this.improvedValue.text = this.improved.currentText; + this.improvedValue.view = this.improved; + current = this.improvedValue; + } + if (ViewCommentDataComponent.checkInputData(current.body, current.text, + this.translateService, this.notificationService, this.data.maxTextCharacters, this.data.maxDataCharacters)) { + this.data.onClose(current.body, current.text, current.view); + this.dialogRef.close(true); + } + }; + } + + openHelp() { + const ref = this.dialog.open(ExplanationDialogComponent, { + autoFocus: false + }); + ref.componentInstance.translateKey = 'explanation.deepl'; + } + + onFormalityChange(formality: string) { + DeepLDialogComponent.generateDeeplDelta(this.deeplService, this.data.body, this.data.usedTarget, formality as FormalityType) + .subscribe(([improvedBody, improvedText]) => { + this.improvedValue.body = improvedBody; + this.improvedValue.text = improvedText; + this.improved.currentData = improvedBody; + }, (_) => { + this.translateService.get('deepl-formality-select.error').subscribe(str => { + this.notificationService.show(str); + }); + }); } } diff --git a/src/app/components/shared/_dialogs/explanation-dialog/explanation-dialog.component.html b/src/app/components/shared/_dialogs/explanation-dialog/explanation-dialog.component.html new file mode 100644 index 0000000000000000000000000000000000000000..c8d8180c91c81537955b2ddfc3f7c37a99e9718b --- /dev/null +++ b/src/app/components/shared/_dialogs/explanation-dialog/explanation-dialog.component.html @@ -0,0 +1,7 @@ +<div mat-dialog-content> + <app-custom-markdown [data]="data"></app-custom-markdown> +</div> +<app-dialog-action-buttons [buttonsLabelSection]="'explanation'" + [confirmButtonLabel]="'close'" + [confirmButtonClickAction]="buildConfirmAction()"> +</app-dialog-action-buttons> diff --git a/src/app/components/shared/_dialogs/explanation-dialog/explanation-dialog.component.scss b/src/app/components/shared/_dialogs/explanation-dialog/explanation-dialog.component.scss new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/src/app/components/shared/_dialogs/explanation-dialog/explanation-dialog.component.spec.ts b/src/app/components/shared/_dialogs/explanation-dialog/explanation-dialog.component.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..24296abb500aa75dde986391ea07d445f1457d49 --- /dev/null +++ b/src/app/components/shared/_dialogs/explanation-dialog/explanation-dialog.component.spec.ts @@ -0,0 +1,26 @@ +/*import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { ExplanationDialogComponent } from './explanation-dialog.component'; + +describe('ExplanationDialogComponent', () => { + let component: ExplanationDialogComponent; + let fixture: ComponentFixture<ExplanationDialogComponent>; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ ExplanationDialogComponent ] + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(ExplanationDialogComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); + */ diff --git a/src/app/components/shared/_dialogs/explanation-dialog/explanation-dialog.component.ts b/src/app/components/shared/_dialogs/explanation-dialog/explanation-dialog.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..3958cb6f594e4f2af5f692a73ba823eb7c3521ce --- /dev/null +++ b/src/app/components/shared/_dialogs/explanation-dialog/explanation-dialog.component.ts @@ -0,0 +1,33 @@ +import { Component, Input, OnInit } from '@angular/core'; +import { TranslateService } from '@ngx-translate/core'; +import { LanguageService } from '../../../../services/util/language.service'; +import { MatDialogRef } from '@angular/material/dialog'; + +@Component({ + selector: 'app-explanation-dialog', + templateUrl: './explanation-dialog.component.html', + styleUrls: ['./explanation-dialog.component.scss'] +}) +export class ExplanationDialogComponent implements OnInit { + + @Input() translateKey: string; + data: string; + + constructor(private translateService: TranslateService, + private languageService: LanguageService, + private dialogRef: MatDialogRef<ExplanationDialogComponent>) { + languageService.langEmitter.subscribe(lang => { + translateService.use(lang); + }); + } + + ngOnInit(): void { + this.translateService.use(localStorage.getItem('currentLang')); + this.translateService.get(this.translateKey).subscribe(text => this.data = text); + } + + buildConfirmAction() { + return () => this.dialogRef.close(); + } + +} diff --git a/src/app/components/shared/_dialogs/quill-input-dialog/quill-input-dialog.component.html b/src/app/components/shared/_dialogs/quill-input-dialog/quill-input-dialog.component.html new file mode 100644 index 0000000000000000000000000000000000000000..591921b2b7061cf427ab4a14d6e3e6498987b2cc --- /dev/null +++ b/src/app/components/shared/_dialogs/quill-input-dialog/quill-input-dialog.component.html @@ -0,0 +1,21 @@ +<h3>{{'quill.heading' | translate}}</h3> +<mat-dialog-content> + <mat-form-field appearance="fill"> + <mat-label>{{'quill.tooltip-label-' + data.type | translate}}</mat-label> + <input type="text" + matInput + autofocus + placeholder="{{'quill.tooltip-placeholder-' + data.type | translate}}" + [(ngModel)]="value"> + </mat-form-field> + <app-custom-markdown [data]="getKatex()" + *ngIf="data.type === 'formula'" + [katexOptions]="katexOptions"> + </app-custom-markdown> +</mat-dialog-content> +<app-dialog-action-buttons + [buttonsLabelSection]="'quill'" + [confirmButtonLabel]="'tooltip-action-save'" + [confirmButtonClickAction]="buildConfirmAction()" + [cancelButtonClickAction]="buildCancelAction()"> +</app-dialog-action-buttons> diff --git a/src/app/components/shared/_dialogs/quill-input-dialog/quill-input-dialog.component.scss b/src/app/components/shared/_dialogs/quill-input-dialog/quill-input-dialog.component.scss new file mode 100644 index 0000000000000000000000000000000000000000..c7acb4bf6e7f6dd478cc87d9fc5a9ee93fd12013 --- /dev/null +++ b/src/app/components/shared/_dialogs/quill-input-dialog/quill-input-dialog.component.scss @@ -0,0 +1,3 @@ +mat-form-field { + width: 100%; +} diff --git a/src/app/components/shared/_dialogs/quill-input-dialog/quill-input-dialog.component.spec.ts b/src/app/components/shared/_dialogs/quill-input-dialog/quill-input-dialog.component.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..a7b4da6e65beb3f73a57be501a41eae40946718b --- /dev/null +++ b/src/app/components/shared/_dialogs/quill-input-dialog/quill-input-dialog.component.spec.ts @@ -0,0 +1,26 @@ +/*import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { QuillInputDialogComponent } from './quill-input-dialog.component'; + +describe('QuillInputDialogComponent', () => { + let component: QuillInputDialogComponent; + let fixture: ComponentFixture<QuillInputDialogComponent>; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ QuillInputDialogComponent ] + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(QuillInputDialogComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); + */ diff --git a/src/app/components/shared/_dialogs/quill-input-dialog/quill-input-dialog.component.ts b/src/app/components/shared/_dialogs/quill-input-dialog/quill-input-dialog.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..e054bde4f3a204390a0769a73e98fa29917b6985 --- /dev/null +++ b/src/app/components/shared/_dialogs/quill-input-dialog/quill-input-dialog.component.ts @@ -0,0 +1,88 @@ +import { Component, Inject, OnInit } from '@angular/core'; +import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; +import Delta from 'quill-delta'; +import { KatexOptions } from 'ngx-markdown'; + +interface DialogData { + type: string; + meta: string; + quill: any; + selection: any; + overrideAction?: (value: string, selection: any) => void; +} + +@Component({ + selector: 'app-quill-input-dialog', + templateUrl: './quill-input-dialog.component.html', + styleUrls: ['./quill-input-dialog.component.scss'] +}) +export class QuillInputDialogComponent implements OnInit { + + value = ''; + katexOptions: KatexOptions = { + throwOnError: false + }; + + constructor(@Inject(MAT_DIALOG_DATA) public data: DialogData, + private dialogRef: MatDialogRef<QuillInputDialogComponent>) { + } + + private static getVideoUrl(url) { + let match = url.match(/^(?:(https?):\/\/)?(?:(?:www|m)\.)?youtube\.com\/watch.*v=([a-zA-Z0-9_-]+)/) || + url.match(/^(?:(https?):\/\/)?(?:(?:www|m)\.)?youtu\.be\/([a-zA-Z0-9_-]+)/) || + url.match(/^.*(youtu.be\/|v\/|e\/|u\/\w+\/|embed\/|v=)([^#&?]*).*/); + if (match && match[2].length === 11) { + return 'https://www.youtube-nocookie.com/embed/' + match[2] + '?showinfo=0'; + } + match = url.match(/^(?:(https?):\/\/)?(?:www\.)?vimeo\.com\/(\d+)/); + if (match) { + return (match[1] || 'https') + '://player.vimeo.com/video/' + match[2] + '/'; + } + return null; + } + + ngOnInit(): void { + this.value = this.data.meta || ''; + } + + getKatex(): string { + return '$' + this.value + '$'; + } + + buildConfirmAction() { + return () => { + if (this.data.overrideAction) { + this.data.overrideAction(this.value, this.data.selection); + this.dialogRef.close(); + return; + } + switch (this.data.type) { + case 'link': + if (this.value) { + const delta = new Delta() + .retain(this.data.selection.index) + .retain(this.data.selection.length, { link: this.value }); + this.data.quill.updateContents(delta); + } + break; + case 'video': + const value = QuillInputDialogComponent.getVideoUrl(this.value) || this.value; + if (value) { + this.data.quill.insertEmbed(this.data.selection.index, 'video', value, 'user'); + } + break; + default: + if (this.value) { + this.data.quill.insertEmbed(this.data.selection.index, this.data.type, this.value, 'user'); + } + break; + } + this.dialogRef.close(); + }; + } + + buildCancelAction() { + return () => this.dialogRef.close(); + } + +} 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 290fdaffbb54c2ace7939b1195e61c785becf998..ed46fd8e49743888ad00a1952d5792ec99713899 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 @@ -27,12 +27,12 @@ <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" - [class.keywords-even]="even" - [ngClass]="{'keyword-selected': keyword.selected, 'first-keyword': i === 0}"> + <mat-list-item *ngFor="let keyword of keywords; let odd = odd; let even = even; let i = index" + [class.keywords-alternate]="odd" + [class.keywords-even]="even" + [ngClass]="{'keyword-selected': keyword.selected, 'first-keyword': i === 0}"> <span class="keyword-span" *ngIf="!keyword.editing">{{keyword.word}}</span> - <input class="keyword-span, isEditing" *ngIf="keyword.editing" [(ngModel)]="keyword.word"/> + <input class="keyword-span, isEditing" *ngIf="keyword.editing" [(ngModel)]="keyword.word"/> <div class="keywords-actions"> <mat-checkbox [checked]="keyword.completed" (change)="keyword.selected = $event.checked" @@ -49,7 +49,7 @@ </button> <button *ngIf="keyword.editing" (click)="onEndEditing(keyword); onEditChange(-1)" mat-icon-button - class = "edit-accept" + class="edit-accept" matTooltip="{{ 'spacy-dialog.editing-done-hint' | translate }}" matTooltipShowDelay="750"> <mat-icon>check</mat-icon> @@ -67,8 +67,18 @@ </div> </ars-row> -<ars-row ars-flex-box> - <ars-fill></ars-fill> +<ars-row ars-flex-box class="action-button-container"> + <ars-col *ngIf="!isLoading && langSupported && hasKeywordsFromSpacy"> + <button + mat-flat-button + class="help-button" + (click)="openHelp()"> + <mat-icon>help</mat-icon> + {{ 'explanation.label' | translate}} + </button> + </ars-col> + <ars-fill *ngIf="isLoading || !langSupported || !hasKeywordsFromSpacy"> + </ars-fill> <ars-col> <app-dialog-action-buttons #appDialogActionButtons diff --git a/src/app/components/shared/_dialogs/spacy-dialog/spacy-dialog.component.scss b/src/app/components/shared/_dialogs/spacy-dialog/spacy-dialog.component.scss index 29d07cae3203aad421599e3cf54125088ef00b0e..1597e65d54d24665affeb25514c41a6c7bbfa563 100644 --- a/src/app/components/shared/_dialogs/spacy-dialog/spacy-dialog.component.scss +++ b/src/app/components/shared/_dialogs/spacy-dialog/spacy-dialog.component.scss @@ -138,3 +138,25 @@ .mat-option { color: var(--on-surface); } + +.help-button { + background-color: var(--secondary); + color: var(--on-secondary); + margin-top: 1rem; + + .mat-icon { + font-size: 18px; + margin-top: 3px; + } +} + +.action-button-container { + @media screen and (max-width: 500px) { + overflow: auto; + display: flex; + justify-content: space-between; + flex-direction: column !important; + flex-wrap: wrap; + align-items: flex-end; + } +} 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 935ba65bcf536bd31513481e887b488080d70913..b59c7c531251796aad8524a6f0bb4f5869e0d53c 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,10 +1,11 @@ import { AfterContentInit, Component, Inject, OnInit, ViewChild } from '@angular/core'; -import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog'; +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 { 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'; export interface Keyword { word: string; @@ -38,6 +39,7 @@ export class SpacyDialogComponent implements OnInit, AfterContentInit { protected langService: LanguagetoolService, private spacyService: SpacyService, public dialogRef: MatDialogRef<SpacyDialogComponent>, + private dialog: MatDialog, @Inject(MAT_DIALOG_DATA) public data) { } @@ -134,7 +136,7 @@ export class SpacyDialogComponent implements OnInit, AfterContentInit { } manualKeywordsToKeywords() { - const tempKeywords = this.manualKeywords.replace(/\s/g, ''); + const tempKeywords = this.manualKeywords.replace(/\s+/g, ' '); if (tempKeywords.length) { this.keywords = tempKeywords.split(',').map((keyword) => ( { @@ -154,4 +156,11 @@ export class SpacyDialogComponent implements OnInit, AfterContentInit { this._concurrentEdits += change; this.appDialogActionButtons.confirmButtonDisabled = (this._concurrentEdits > 0); } + + openHelp() { + const ref = this.dialog.open(ExplanationDialogComponent, { + autoFocus: false + }); + ref.componentInstance.translateKey = 'explanation.spacy'; + } } diff --git a/src/app/components/shared/_dialogs/topic-cloud-administration/topic-cloud-administration.component.scss b/src/app/components/shared/_dialogs/topic-cloud-administration/topic-cloud-administration.component.scss index 86c644ce629cd8a7704e986c99957c3071e622fb..d2d7016945f138f7beb5eee32166f0a01caa6b09 100644 --- a/src/app/components/shared/_dialogs/topic-cloud-administration/topic-cloud-administration.component.scss +++ b/src/app/components/shared/_dialogs/topic-cloud-administration/topic-cloud-administration.component.scss @@ -177,7 +177,12 @@ mat-dialog-content { .reset { margin: 25px auto auto auto; - background-color: var(--secondary); - color: black; + background-color: var(--primary); + color: var(--on-primary); width: 100%; + + &:focus { + background-color: var(--secondary); + color: var(--on-secondary); + } } diff --git a/src/app/components/shared/_dialogs/topic-cloud-filter/topic-cloud-filter.component.html b/src/app/components/shared/_dialogs/topic-cloud-filter/topic-cloud-filter.component.html index f5e3f893c2d878a85eafe33baf9ad06610279ed7..b9c538b80c95c353e76c848b174d44ade4a5506c 100644 --- a/src/app/components/shared/_dialogs/topic-cloud-filter/topic-cloud-filter.component.html +++ b/src/app/components/shared/_dialogs/topic-cloud-filter/topic-cloud-filter.component.html @@ -93,13 +93,26 @@ <app-worker-dialog [inlined]="true" *ngIf="user && user.role > 0"></app-worker-dialog> - <app-dialog-action-buttons - buttonsLabelSection="content" - confirmButtonLabel="tag-cloud-create" - buttonIcon="cloud" - - [cancelButtonClickAction]="cancelButtonActionCallback()" - [confirmButtonClickAction]="confirmButtonActionCallback()"> - </app-dialog-action-buttons> + <ars-row ars-flex-box class="action-button-container"> + <ars-col> + <button + mat-flat-button + class="help-button" + (click)="openHelp()"> + <mat-icon>help</mat-icon> + {{ 'explanation.label' | translate}} + </button> + </ars-col> + <ars-col> + <app-dialog-action-buttons + buttonsLabelSection="content" + confirmButtonLabel="tag-cloud-create" + buttonIcon="cloud" + [spacing]="false" + [cancelButtonClickAction]="cancelButtonActionCallback()" + [confirmButtonClickAction]="confirmButtonActionCallback()"> + </app-dialog-action-buttons> + </ars-col> + </ars-row> </mat-dialog-content> diff --git a/src/app/components/shared/_dialogs/topic-cloud-filter/topic-cloud-filter.component.scss b/src/app/components/shared/_dialogs/topic-cloud-filter/topic-cloud-filter.component.scss index 35e221f156f660e8d1ddc7cf4d6ef276e7bc4746..37a1681103021591609e33d4a80e2382b2db8fbe 100644 --- a/src/app/components/shared/_dialogs/topic-cloud-filter/topic-cloud-filter.component.scss +++ b/src/app/components/shared/_dialogs/topic-cloud-filter/topic-cloud-filter.component.scss @@ -49,3 +49,25 @@ mat-radio-group { background-color: var(--secondary); color: var(--on-secondary); } + +.help-button { + background-color: var(--secondary); + color: var(--on-secondary); + margin-top: 1rem; + + .mat-icon { + font-size: 18px; + margin-top: 3px; + } +} + +.action-button-container { + @media screen and (max-width: 500px) { + overflow: auto; + display: flex; + justify-content: space-between; + flex-direction: column !important; + flex-wrap: wrap; + align-items: flex-end; + } +} diff --git a/src/app/components/shared/_dialogs/topic-cloud-filter/topic-cloud-filter.component.ts b/src/app/components/shared/_dialogs/topic-cloud-filter/topic-cloud-filter.component.ts index 769f5d25a10dbe4577d94428659968d12477551c..b02423352836c29705e0318208e0fecb31dd6e81 100644 --- a/src/app/components/shared/_dialogs/topic-cloud-filter/topic-cloud-filter.component.ts +++ b/src/app/components/shared/_dialogs/topic-cloud-filter/topic-cloud-filter.component.ts @@ -11,14 +11,14 @@ import { RoomService } from '../../../../services/http/room.service'; import { Comment } from '../../../../models/comment'; import { CommentListData } from '../../comment-list/comment-list.component'; import { TopicCloudAdminService } from '../../../../services/util/topic-cloud-admin.service'; -import { KeywordOrFulltext, TopicCloudAdminData } from '../topic-cloud-administration/TopicCloudAdminData'; +import { TopicCloudAdminData } from '../topic-cloud-administration/TopicCloudAdminData'; import { TagCloudDataService } from '../../../../services/util/tag-cloud-data.service'; import { User } from '../../../../models/user'; import { WorkerDialogComponent } from '../worker-dialog/worker-dialog.component'; import { Room } from '../../../../models/room'; -import { CloudParameters } from '../../../../utils/cloud-parameters'; import { ThemeService } from '../../../../../theme/theme.service'; import { Theme } from '../../../../../theme/Theme'; +import { ExplanationDialogComponent } from '../explanation-dialog/explanation-dialog.component'; class CommentsCount { comments: number; @@ -157,6 +157,13 @@ export class TopicCloudFilterComponent implements OnInit { } } + openHelp() { + const ref = this.dialog.open(ExplanationDialogComponent, { + autoFocus: false + }); + ref.componentInstance.translateKey = 'explanation.topic-cloud'; + } + private isUpdatable(): boolean { if (this.comments.length < 3) { return 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 62091d33b420a05ab2019fe8f60496aa458abd53..30778ca4cd9324909a88c076a3edefa5578a05e8 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 @@ -7,6 +7,7 @@ import { CreateCommentKeywords } 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'; const concurrentCallsPerTask = 4; @@ -59,7 +60,8 @@ export class WorkerDialogTask { return; } const currentComment = this._comments[currentIndex]; - CreateCommentKeywords.isSpellingAcceptable(this.languagetoolService, currentComment.body) + const text = ViewCommentDataComponent.getTextFromData(currentComment.body); + CreateCommentKeywords.isSpellingAcceptable(this.languagetoolService, text) .subscribe(result => { if (!result.isAcceptable) { this.finishSpacyCall(FinishType.badSpelled, currentIndex); diff --git a/src/app/components/shared/comment-answer/comment-answer.component.scss b/src/app/components/shared/comment-answer/comment-answer.component.scss index 17448b00544cb65fb8212e99b507b783ed8596e4..4d6b527f1ccc6a1d6309e10c960a6eda1b226434 100644 --- a/src/app/components/shared/comment-answer/comment-answer.component.scss +++ b/src/app/components/shared/comment-answer/comment-answer.component.scss @@ -37,5 +37,5 @@ mat-icon { } .border-answer { - 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); + 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), -8px 0 0 0 var(--primary); } diff --git a/src/app/components/shared/comment-list/comment-list.component.html b/src/app/components/shared/comment-list/comment-list.component.html index 4d7ee86513995f59604f897ecc41eb131ae01b5d..e3969fc569c71acbcabfad83d2e9e8fe67289870 100644 --- a/src/app/components/shared/comment-list/comment-list.component.html +++ b/src/app/components/shared/comment-list/comment-list.component.html @@ -266,7 +266,7 @@ <div *ngIf="!isLoading"> <app-comment *ngFor="let current of hideCommentsList ? filteredComments : commentsFilteredByTime; let i = index" - [appScrollIntoView]="current.id === newCommentId" + [appScrollIntoView]="current.id === focusCommentId" [usesJoyride]="i === 0" [comment]="current" [parseVote]="getVote(current)" @@ -276,7 +276,8 @@ [disabled]="!commentsEnabled" (clickedOnTag)="clickedOnTag($event)" (clickedUserNumber)="clickedUserNumber($event)" - (clickedOnKeyword)="clickedOnKeyword($event)"> + (clickedOnKeyword)="clickedOnKeyword($event)" + (votedComment)="votedComment($event)"> </app-comment> </div> diff --git a/src/app/components/shared/comment-list/comment-list.component.ts b/src/app/components/shared/comment-list/comment-list.component.ts index 8bd4894506f4c096001b6524d1c3ca7a808f15ed..231859ccc78af5650241bf1c2140cb851df33f2c 100644 --- a/src/app/components/shared/comment-list/comment-list.component.ts +++ b/src/app/components/shared/comment-list/comment-list.component.ts @@ -1,4 +1,4 @@ -import { Component, ElementRef, Input, OnInit, OnDestroy, ViewChild } from '@angular/core'; +import { Component, ElementRef, Input, OnDestroy, OnInit, ViewChild } from '@angular/core'; import { Comment } from '../../../models/comment'; import { CommentService } from '../../../services/http/comment.service'; import { TranslateService } from '@ngx-translate/core'; @@ -16,7 +16,7 @@ import { LiveAnnouncer } from '@angular/cdk/a11y'; import { EventService } from '../../../services/util/event.service'; import { Subscription } from 'rxjs'; import { AppComponent } from '../../../app.component'; -import { Router, ActivatedRoute } from '@angular/router'; +import { ActivatedRoute, Router } from '@angular/router'; import { AuthenticationService } from '../../../services/http/authentication.service'; import { TitleService } from '../../../services/util/title.service'; import { ModeratorsComponent } from '../../creator/_dialogs/moderators/moderators.component'; @@ -32,6 +32,7 @@ import { RoomDataService } from '../../../services/util/room-data.service'; import { WsRoomService } from '../../../services/websockets/ws-room.service'; import { ActiveUserService } from '../../../services/http/active-user.service'; import { OnboardingService } from '../../../services/util/onboarding.service'; +import { WorkerDialogComponent } from '../_dialogs/worker-dialog/worker-dialog.component'; export interface CommentListData { comments: Comment[]; @@ -103,7 +104,7 @@ export class CommentListComponent implements OnInit, OnDestroy { userNumberSelection = 0; createCommentWrapper: CreateCommentWrapper = null; isJoyrideActive = false; - newCommentId = ''; + focusCommentId = ''; private _subscriptionEventServiceTagConfig = null; private _subscriptionEventServiceRoomData = null; private _subscriptionRoomService = null; @@ -275,6 +276,7 @@ export class CommentListComponent implements OnInit, OnDestroy { return; } this.comments = comments; + this.generateKeywordsIfEmpty(); this.getComments(); this.eventService.broadcast('commentListCreated', null); this.isJoyrideActive = this.onboardingService.startDefaultTour(); @@ -429,14 +431,14 @@ export class CommentListComponent implements OnInit, OnDestroy { return c.createdFromLecturer; } }); - const testForModerator=()=>{ - this.comments.forEach(e=>{ - this.commentService.role(e).subscribe(i=>{ - console.log(e,i); + const testForModerator = () => { + this.comments.forEach(e => { + this.commentService.role(e).subscribe(i => { + console.log(e, i); }); }); }; - if(type==='moderator'){ + if (type === 'moderator') { console.log( 'TEST moderator', this.moderatorIds, @@ -490,6 +492,10 @@ export class CommentListComponent implements OnInit, OnDestroy { this.filterComments(this.userNumber, usrNumber); } + votedComment(voteInfo: string) { + setTimeout(() => this.focusCommentId = voteInfo, 100); + } + pauseCommentStream() { this.freeze = true; this.roomDataService.getRoomData(this.roomId, true).subscribe(comments => { @@ -563,7 +569,7 @@ export class CommentListComponent implements OnInit, OnDestroy { writeComment() { this.createCommentWrapper.openCreateDialog(this.user) - .subscribe(comment => this.newCommentId = comment && comment.id); + .subscribe(comment => this.focusCommentId = comment && comment.id); } /** @@ -643,4 +649,15 @@ export class CommentListComponent implements OnInit, OnDestroy { return filter; } + + private generateKeywordsIfEmpty() { + if (this.comments.length > 0 && this.userRole === UserRole.CREATOR) { + const count = this.comments.reduce((acc, comment) => + acc + (comment.keywordsFromQuestioner && comment.keywordsFromQuestioner.length) + + (comment.keywordsFromSpacy && comment.keywordsFromSpacy.length), 0); + if (count < 1) { + WorkerDialogComponent.addWorkTask(this.dialog, this.room); + } + } + } } diff --git a/src/app/components/shared/comment/comment.component.scss b/src/app/components/shared/comment/comment.component.scss index e141aa117ee24a56d51339824c27bcb1c29f0d6c..8dfd102627b6a5d228b0c2af8548c02b5c8533d1 100644 --- a/src/app/components/shared/comment/comment.component.scss +++ b/src/app/components/shared/comment/comment.component.scss @@ -1,4 +1,4 @@ -@mixin card-box-shadow($color: transparent, $x: -4px) { +@mixin card-box-shadow($color: transparent, $x: -8px) { box-shadow: 0px 2px 1px -1px rgba(0, 0, 0, 0.2), 0px 1px 1px 0px rgba(0, 0, 0, 0.14), 0px 1px 3px 0px rgba(0, 0, 0, 0.12), diff --git a/src/app/components/shared/comment/comment.component.ts b/src/app/components/shared/comment/comment.component.ts index 03c6c3d65d58e3e277fb57308a9df03ee0e7ae97..bf362b929c889b1a86a796475cdb06d469121dfa 100644 --- a/src/app/components/shared/comment/comment.component.ts +++ b/src/app/components/shared/comment/comment.component.ts @@ -47,6 +47,7 @@ export class CommentComponent implements OnInit, AfterViewInit, OnDestroy { @Output() clickedOnTag = new EventEmitter<string>(); @Output() clickedOnKeyword = new EventEmitter<string>(); @Output() clickedUserNumber = new EventEmitter<number>(); + @Output() votedComment = new EventEmitter<string>(); @ViewChild('commentBody', { static: true })commentBody: RowComponent; @ViewChild('commentBodyInner', { static: true })commentBodyInner: RowComponent; @ViewChild('commentExpander', { static: true })commentExpander: RowComponent; @@ -205,11 +206,11 @@ export class CommentComponent implements OnInit, AfterViewInit, OnDestroy { voteUp(comment: Comment): void { const userId = this.authenticationService.getUser().id; if (this.hasVoted !== 1) { - this.commentService.voteUp(comment, userId).subscribe(); + this.commentService.voteUp(comment, userId).subscribe(_ => this.votedComment.emit(this.comment.id)); this.hasVoted = 1; this.currentVote = '1'; } else { - this.commentService.resetVote(comment, userId).subscribe(); + this.commentService.resetVote(comment, userId).subscribe(_ => this.votedComment.emit(this.comment.id)); this.hasVoted = 0; this.currentVote = '0'; } @@ -219,11 +220,11 @@ export class CommentComponent implements OnInit, AfterViewInit, OnDestroy { voteDown(comment: Comment): void { const userId = this.authenticationService.getUser().id; if (this.hasVoted !== -1) { - this.commentService.voteDown(comment, userId).subscribe(); + this.commentService.voteDown(comment, userId).subscribe(_ => this.votedComment.emit(this.comment.id)); this.hasVoted = -1; this.currentVote = '-1'; } else { - this.commentService.resetVote(comment, userId).subscribe(); + this.commentService.resetVote(comment, userId).subscribe(_ => this.votedComment.emit(this.comment.id)); this.hasVoted = 0; this.currentVote = '0'; } diff --git a/src/app/components/shared/dialog/topic-dialog-comment/topic-dialog-comment.component.ts b/src/app/components/shared/dialog/topic-dialog-comment/topic-dialog-comment.component.ts index 77b32abda5c7f29d5fa3b4d559e558e36d3ad61a..438c5192aaaa1846215b6f97d4861d048a3df251 100644 --- a/src/app/components/shared/dialog/topic-dialog-comment/topic-dialog-comment.component.ts +++ b/src/app/components/shared/dialog/topic-dialog-comment/topic-dialog-comment.component.ts @@ -1,6 +1,7 @@ import { Component, Input, OnInit } from '@angular/core'; import { Language } from '../../../../models/comment'; import { ProfanityFilterService } from '../../../../services/util/profanity-filter.service'; +import { ViewCommentDataComponent } from '../../view-comment-data/view-comment-data.component'; @Component({ selector: 'app-topic-dialog-comment', @@ -49,6 +50,7 @@ export class TopicDialogCommentComponent implements OnInit { if (!this.language) { return; } + this.question = ViewCommentDataComponent.getTextFromData(this.question); this.questionWithoutProfanity = this.profanityFilterService. filterProfanityWords(this.question, this.partialWords, this.languageSpecific, this.language); this.partsWithoutProfanity = this.splitQuestion(this.questionWithoutProfanity); diff --git a/src/app/components/shared/questionwall/question-wall/question-wall.component.scss b/src/app/components/shared/questionwall/question-wall/question-wall.component.scss index e8fa88e8d738caa1efb8e2b08d086d3ae3328056..9be9d93aa35bdb7dcb5014fe38fdf45a85cccd1c 100644 --- a/src/app/components/shared/questionwall/question-wall/question-wall.component.scss +++ b/src/app/components/shared/questionwall/question-wall/question-wall.component.scss @@ -243,7 +243,7 @@ &-title { font-size: 35px; padding: 32px 32px 0 32px; - color: var(--ars-header-color); + color: yellow; } &-desc { diff --git a/src/app/components/shared/shared.module.ts b/src/app/components/shared/shared.module.ts index 75057040a34d0062331612f014eddd78c0d7fdd7..7637d1bf80426a544fd9c61ea8c8aa2efac520bd 100644 --- a/src/app/components/shared/shared.module.ts +++ b/src/app/components/shared/shared.module.ts @@ -52,6 +52,8 @@ import { ScrollIntoViewDirective } from '../../directives/scroll-into-view.direc import { QuillModule } from 'ngx-quill'; import { ViewCommentDataComponent } from './view-comment-data/view-comment-data.component'; import { DeepLDialogComponent } from './_dialogs/deep-ldialog/deep-ldialog.component'; +import { ExplanationDialogComponent } from './_dialogs/explanation-dialog/explanation-dialog.component'; +import { QuillInputDialogComponent } from './_dialogs/quill-input-dialog/quill-input-dialog.component'; @NgModule({ imports: [ @@ -109,7 +111,9 @@ import { DeepLDialogComponent } from './_dialogs/deep-ldialog/deep-ldialog.compo CustomMarkdownComponent, ScrollIntoViewDirective, ViewCommentDataComponent, - DeepLDialogComponent + DeepLDialogComponent, + ExplanationDialogComponent, + QuillInputDialogComponent ], exports: [ RoomJoinComponent, diff --git a/src/app/components/shared/tag-cloud/tag-cloud.component.scss b/src/app/components/shared/tag-cloud/tag-cloud.component.scss index e277c89c4e79591d037bb07a1d02498f6a39b206..b0317c3ab0c2a5eba74450b66d8a8de40925ced7 100644 --- a/src/app/components/shared/tag-cloud/tag-cloud.component.scss +++ b/src/app/components/shared/tag-cloud/tag-cloud.component.scss @@ -66,6 +66,6 @@ app-tag-cloud-pop-up { text-transform: var(--tag-cloud-transform, unset); font-weight: var(--tag-cloud-font-weight, normal); font-style: var(--tag-cloud-font-style, normal); - font-family: var(--tag-cloud-font-family, 'Dancing Script'); + font-family: var(--tag-cloud-font-family, 'sans-serif'); font-size: 50px; } diff --git a/src/app/components/shared/view-comment-data/view-comment-data.component.html b/src/app/components/shared/view-comment-data/view-comment-data.component.html index df7e8b0c54aca137defbe59559438a755877a67f..e535dc8f7f0f4b1ff5c61e779790aa1113d40346 100644 --- a/src/app/components/shared/view-comment-data/view-comment-data.component.html +++ b/src/app/components/shared/view-comment-data/view-comment-data.component.html @@ -3,7 +3,12 @@ <quill-editor #editor placeholder="{{ placeHolderText | translate }}" [modules]="quillModules" - (document:click)="onDocumentClick($event)"> + (document:click)="onDocumentClick($event)" + (window:resize)="recalcAspectRatio()"> + <div quill-editor-toolbar> + <ng-container [ngTemplateOutlet]="isModerator ? moderatorToolbar : participantToolbar"> + </ng-container> + </div> </quill-editor> <div #tooltipContainer></div> <div fxLayout="row" style="justify-content: flex-end; padding: 0 5px"> @@ -13,6 +18,64 @@ </div> </ars-row> <div *ngIf="!isEditor"> - <quill-view #quillView [modules]="quillModules"> + <quill-view #quillView [modules]="quillModules" (window:resize)="recalcAspectRatio()"> </quill-view> </div> + +<ng-template #participantToolbar> + <span class="ql-formats"> + <button type="button" class="ql-bold"></button> + <button type="button" class="ql-list" value="bullet"></button> + <button type="button" class="ql-list" value="ordered"></button> + <button type="button" class="ql-blockquote"></button> + <button type="button" class="ql-link" (click)="onClick($event, 'link')"></button> + <button type="button" class="ql-code-block"></button> + <button type="button" class="ql-formula" (click)="onClick($event, 'formula')"></button> + <button type="button" class="ql-emoji" *ngIf="hasEmoji"></button> + <mat-form-field class="deepl-form-field" *ngIf="usesFormality"> + <mat-label> + {{'deepl-formality-select.name' | translate}} + </mat-label> + <label for="deeplFormality"> + {{selectedFormality && ('deepl-formality-select.' + selectedFormality | translate)}} + </label> + <mat-select id="deeplFormality" [(ngModel)]="selectedFormality" + (selectionChange)="formalityEmitter && formalityEmitter(selectedFormality)"> + <mat-option value="default">{{'deepl-formality-select.default' | translate}}</mat-option> + <mat-option value="less">{{'deepl-formality-select.less' | translate}}</mat-option> + <mat-option value="more">{{'deepl-formality-select.more' | translate}}</mat-option> + </mat-select> + </mat-form-field> + </span> +</ng-template> + +<ng-template #moderatorToolbar> + <span class="ql-formats"> + <button type="button" class="ql-bold"></button> + <select class="ql-color"></select> + <button type="button" class="ql-strike"></button> + <button type="button" class="ql-list" value="bullet"></button> + <button type="button" class="ql-list" value="ordered"></button> + <button type="button" class="ql-blockquote"></button> + <button type="button" class="ql-link" (click)="onClick($event, 'link')"></button> + <button type="button" class="ql-image" (click)="onClick($event, 'image')"></button> + <button type="button" class="ql-video" (click)="onClick($event, 'video')"></button> + <button type="button" class="ql-code-block"></button> + <button type="button" class="ql-formula" (click)="onClick($event, 'formula')"></button> + <button type="button" class="ql-emoji" *ngIf="hasEmoji"></button> + <mat-form-field class="deepl-form-field" *ngIf="usesFormality"> + <mat-label> + {{'deepl-formality-select.name' | translate}} + </mat-label> + <label for="deeplFormality"> + {{selectedFormality && ('deepl-formality-select.' + selectedFormality | translate)}} + </label> + <mat-select id="deeplFormality" [(ngModel)]="selectedFormality" + (selectionChange)="formalityEmitter && formalityEmitter(selectedFormality)"> + <mat-option value="default">{{'deepl-formality-select.default' | translate}}</mat-option> + <mat-option value="less">{{'deepl-formality-select.less' | translate}}</mat-option> + <mat-option value="more">{{'deepl-formality-select.more' | translate}}</mat-option> + </mat-select> + </mat-form-field> + </span> +</ng-template> diff --git a/src/app/components/shared/view-comment-data/view-comment-data.component.scss b/src/app/components/shared/view-comment-data/view-comment-data.component.scss index f5d4687c77c7678f6361d27223d69c56e3d4b729..bdba3c270cc92c4c447f8726c3b8294718c53c7c 100644 --- a/src/app/components/shared/view-comment-data/view-comment-data.component.scss +++ b/src/app/components/shared/view-comment-data/view-comment-data.component.scss @@ -42,22 +42,6 @@ } .ql-tooltip { - &[data-mode=formula]::before { - --quill-tooltip-label: var(--quill-tooltip-label-formula); - } - - &[data-mode=video]::before { - --quill-tooltip-label: var(--quill-tooltip-label-video); - } - - &[data-mode=image]::before { - --quill-tooltip-label: var(--quill-tooltip-label-image); - } - - &[data-mode=link]::before { - --quill-tooltip-label: var(--quill-tooltip-label-link); - } - &::before { content: var(--quill-tooltip-label) !important; } @@ -118,6 +102,7 @@ .ql-formats { margin-right: 0; + width: 100%; > * { @media only screen and (max-device-width: 480px) and (orientation: portrait), @@ -158,3 +143,56 @@ } } } + +#deeplFormality { + display: inline; +} + +.deepl-form-field { + @media screen and (max-width: 500px) { + width: 100px; + } + z-index: 10000; + height: 1em; + float: right; + margin-right: 0 !important; +} + +::ng-deep .deepl-form-field { + .mat-form-field-wrapper { + position: absolute; + top: -1.35em; + width: fit-content; + right: 0; + + @media only screen and (max-device-width: 480px) and (orientation: portrait), + only screen and (max-device-height: 480px) and (orientation: landscape) { + top: -1em; + } + } + + .mat-form-field-infix { + width: max-content; + } +} + +.anchor-right { + @media screen and (max-width: 500px) { + width: 70px; + left: calc(100% - 70px); + } + width: 200px; + height: 50px; + position: relative; + left: calc(100% - 200px); + top: 0; +} + +.anchor-wrp { + width: calc(100% - 200px); + display: inline-block; + height: 0; + position: relative; + left: 0; + top: 0; +} diff --git a/src/app/components/shared/view-comment-data/view-comment-data.component.ts b/src/app/components/shared/view-comment-data/view-comment-data.component.ts index ccf7af27b688a1fec6c6c847d6317e249445ed3c..4dbd54fbd8c5c4166b7d4b710caab4145c27fcbe 100644 --- a/src/app/components/shared/view-comment-data/view-comment-data.component.ts +++ b/src/app/components/shared/view-comment-data/view-comment-data.component.ts @@ -1,4 +1,11 @@ -import { AfterViewInit, Component, ElementRef, Input, OnInit, ViewChild } from '@angular/core'; +import { + AfterViewInit, + Component, + ElementRef, + Input, + OnInit, + ViewChild, +} from '@angular/core'; import { QuillEditorComponent, QuillModules, QuillViewComponent } from 'ngx-quill'; import Delta from 'quill-delta'; import Quill from 'quill'; @@ -7,18 +14,14 @@ import 'quill-emoji/dist/quill-emoji.js'; import { LanguageService } from '../../../services/util/language.service'; import { TranslateService } from '@ngx-translate/core'; import { DeviceInfoService } from '../../../services/util/device-info.service'; +import { MatDialog } from '@angular/material/dialog'; +import { QuillInputDialogComponent } from '../_dialogs/quill-input-dialog/quill-input-dialog.component'; +import { Marks } from './view-comment-data.marks'; +import { LanguagetoolResult } from '../../../services/http/languagetool.service'; +import { NotificationService } from '../../../services/util/notification.service'; Quill.register('modules/imageResize', ImageResize); -const participantToolbar = [ - ['bold', { list: 'bullet' }, { list: 'ordered' }, 'blockquote', 'link', 'code-block', 'formula', 'emoji'] -]; - -const moderatorToolbar = [ - ['bold', { color: [] }, 'strike', { list: 'bullet' }, { list: 'ordered' }, 'blockquote', - 'link', 'image', 'video', 'code-block', 'formula', 'emoji'], -]; - @Component({ selector: 'app-view-comment-data', templateUrl: './view-comment-data.component.html', @@ -48,29 +51,20 @@ export class ViewCommentDataComponent implements OnInit, AfterViewInit { @Input() maxTextCharacters = 500; @Input() maxDataCharacters = 1500; @Input() placeHolderText = ''; - @Input() markEvents?: { - onCreate: (markContainer: HTMLDivElement, tooltipContainer: HTMLDivElement, editor: QuillEditorComponent) => void; - onChange: (delta: any) => void; - onEditorChange: () => void; - onDocumentClick: (e) => void; - }; + @Input() afterEditorInit?: () => void; + @Input() usesFormality = false; + @Input() formalityEmitter: (string) => void; + @Input() selectedFormality = 'default'; currentText = '\n'; - quillModules: QuillModules = { - toolbar: { - container: participantToolbar, - handlers: { - image: () => this.handle('image'), - video: () => this.handle('video'), - link: () => this.handleLink(), - formula: () => this.handle('formula') - } - } - }; + quillModules: QuillModules = {}; + hasEmoji = true; private _currentData = null; + private _marks: Marks; constructor(private languageService: LanguageService, private translateService: TranslateService, - private deviceInfo: DeviceInfoService) { + private deviceInfo: DeviceInfoService, + private dialog: MatDialog) { this.languageService.langEmitter.subscribe(lang => { this.translateService.use(lang); if (this.isEditor) { @@ -79,6 +73,32 @@ export class ViewCommentDataComponent implements OnInit, AfterViewInit { }); } + public static checkInputData(data: string, + text: string, + translateService: TranslateService, + notificationService: NotificationService, + maxTextCharacters: number, + maxDataCharacters: number): boolean { + text = text.trim(); + if (text.length < 1 && data.length < 1) { + translateService.get('comment-page.error-comment').subscribe(message => { + notificationService.show(message); + }); + return false; + } else if (text.length > maxTextCharacters) { + translateService.get('comment-page.error-comment-text').subscribe(message => { + notificationService.show(message); + }); + return false; + } else if (data.length > maxDataCharacters) { + translateService.get('comment-page.error-comment-data').subscribe(message => { + notificationService.show(message); + }); + return false; + } + return true; + } + public static getDataFromDelta(contentDelta) { return JSON.stringify(contentDelta.ops.map(op => { let hasOnlyInsert = true; @@ -104,10 +124,19 @@ export class ViewCommentDataComponent implements OnInit, AfterViewInit { }; } + public static getTextFromData(jsonData: string): string { + return JSON.parse(jsonData).reduce((acc, e) => { + if (typeof e['insert'] === 'string') { + return acc + e['insert']; + } else if (typeof e === 'string') { + return acc + e; + } + return acc; + }, ''); + } + + ngOnInit(): void { - if (this.isModerator) { - this.quillModules.toolbar['container'] = moderatorToolbar; - } const isMobile = this.deviceInfo.isUserAgentMobile; if (this.isEditor) { this.quillModules['emoji-toolbar'] = !isMobile; @@ -115,6 +144,7 @@ export class ViewCommentDataComponent implements OnInit, AfterViewInit { this.quillModules.imageResize = { modules: ['Resize', 'DisplaySize'] }; + this.hasEmoji = !isMobile; } this.translateService.use(localStorage.getItem('currentLang')); if (this.isEditor) { @@ -125,27 +155,40 @@ export class ViewCommentDataComponent implements OnInit, AfterViewInit { ngAfterViewInit() { if (this.isEditor) { this.editor.onContentChanged.subscribe(e => { - if (this.markEvents && this.markEvents.onChange) { - this.markEvents.onChange(e.delta); - } + this._marks.onDataChange(e.delta); this._currentData = ViewCommentDataComponent.getDataFromDelta(e.content); this.currentText = e.text; + // remove background + const data = e.content; + let changed = false; + data.ops.forEach(op => { + if (op.attributes && op.attributes.background) { + changed = true; + op.attributes.background = null; + delete op.attributes.background; + } + }); + if (changed) { + this.editor.quillEditor.setContents(data); + } }); this.editor.onEditorCreated.subscribe(_ => { - if (this.markEvents && this.markEvents.onCreate) { - this.markEvents.onCreate(this.editorErrorLayer.nativeElement, this.tooltipContainer.nativeElement, this.editor); - } + this._marks = new Marks(this.editorErrorLayer.nativeElement, this.tooltipContainer.nativeElement, this.editor); if (this._currentData) { this.set(this._currentData); } (this.editor.editorElem.firstElementChild as HTMLElement).focus(); + this.overrideQuillTooltip(); this.syncErrorLayer(); - setTimeout(() => this.syncErrorLayer(), 200); // animations? + setTimeout(() => { + this.syncErrorLayer(); + if (this.afterEditorInit) { + this.afterEditorInit(); + } + }, 200); // animations? }); this.editor.onEditorChanged.subscribe(_ => { - if (this.markEvents && this.markEvents.onEditorChange) { - this.markEvents.onEditorChange(); - } + this._marks.sync(); this.syncErrorLayer(); const elem: HTMLDivElement = document.querySelector('div.ql-tooltip'); if (elem) { @@ -171,9 +214,11 @@ export class ViewCommentDataComponent implements OnInit, AfterViewInit { } onDocumentClick(e) { - if (this.markEvents && this.markEvents.onDocumentClick) { - this.markEvents.onDocumentClick(e); + if (!this._marks) { + return; } + const range = this.editor.quillEditor.getSelection(false); + this._marks.onClick(range && range.length === 0 ? range.index : null); } clear(): void { @@ -192,76 +237,104 @@ export class ViewCommentDataComponent implements OnInit, AfterViewInit { } else { this.quillView.quillEditor.setContents(delta); } + this.recalcAspectRatio(); } - private syncErrorLayer(): void { - const pos = this.editor.elementRef.nativeElement.getBoundingClientRect(); - const elem = this.editorErrorLayer.nativeElement; - elem.style.width = pos.width + 'px'; - elem.style.height = pos.height + 'px'; - elem.style.marginBottom = '-' + elem.style.height; + recalcAspectRatio() { + const elem = this.isEditor ? this.editor.editorElem.firstElementChild : this.quillView.editorElem.firstElementChild; + elem.querySelectorAll('.images .ql-video').forEach((e: HTMLElement) => { + const width = parseFloat(window.getComputedStyle(e).width); + e.style.height = (width * 9 / 16) + 'px'; + }); } - private handleLink(): void { - const quill = this.editor.quillEditor; - const selection = quill.getSelection(false); - if (!selection || !selection.length) { + buildMarks(text: string, result: LanguagetoolResult) { + this._marks.buildErrors(text, result); + } + + copyMarks(viewCommentData: ViewCommentDataComponent) { + if (viewCommentData === this) { return; } - const tooltip = quill.theme.tooltip; - const originalSave = tooltip.save; - const originalHide = tooltip.hide; - tooltip.save = () => { - const value = tooltip.textbox.value; - if (value) { - const delta = new Delta() - .retain(selection.index) - .retain(selection.length, { link: value }); - quill.updateContents(delta); - tooltip.hide(); + this._marks.copy(viewCommentData._marks); + } + + public onClick(e: MouseEvent, type) { + e.preventDefault(); + e.stopImmediatePropagation(); + this.handle(type); + return false; + } + + private overrideQuillTooltip() { + const tooltip = this.editor.quillEditor.theme.tooltip; + const prev = tooltip.show.bind(tooltip); + let range; + tooltip.show = () => { + const sel = this.editor.quillEditor.getSelection(false); + const delta = this.editor.quillEditor.getContents(); + let currentSize = 0; + for (const op of delta.ops) { + if (typeof op['insert'] === 'string') { + const start = currentSize; + const len = op['insert'].length; + currentSize += len; + if (sel.index < currentSize) { + range = { index: start, length: len }; + break; + } + } else { + currentSize += 1; + } } + prev(); }; - // Called on hide and save. - tooltip.hide = () => { - tooltip.save = originalSave; - tooltip.hide = originalHide; - tooltip.hide(); + tooltip.edit = (type: string, value: string) => { + this.handle(type, value, (val: string) => { + const delta = new Delta() + .retain(range.index) + .retain(range.length, { link: val }); + this.editor.quillEditor.updateContents(delta); + }); }; - tooltip.edit('link'); - tooltip.textbox.value = quill.getText(selection.index, selection.length); - this.translateService.get('quill.tooltip-placeholder-link') - .subscribe(translation => tooltip.textbox.placeholder = translation); } - private handle(type: string): void { + private handle(type: string, overrideMeta = '', overrideAction = null) { const quill = this.editor.quillEditor; - const tooltip = quill.theme.tooltip; - const originalSave = tooltip.save; - const originalHide = tooltip.hide; - tooltip.save = () => { - const range = quill.getSelection(true); - const value = tooltip.textbox.value; - if (value) { - quill.insertEmbed(range.index, type, value, 'user'); + let meta: any = null; + const selection = quill.getSelection(false); + if (overrideMeta) { + meta = overrideMeta; + } else if (type === 'link') { + if (!selection || !selection.length) { + return; } - }; - // Called on hide and save. - tooltip.hide = () => { - tooltip.save = originalSave; - tooltip.hide = originalHide; - tooltip.hide(); - }; - tooltip.edit(type); - this.translateService.get('quill.tooltip-placeholder-' + type) - .subscribe(translation => tooltip.textbox.placeholder = translation); + } + this.dialog.open(QuillInputDialogComponent, { + width: '900px', + maxWidth: '100%', + maxHeight: 'calc( 100vh - 20px )', + autoFocus: false, + data: { + type, + selection, + quill, + meta, + overrideAction + } + }); + } + + private syncErrorLayer(): void { + const pos = this.editor.elementRef.nativeElement.getBoundingClientRect(); + const elem = this.editorErrorLayer.nativeElement; + elem.style.width = pos.width + 'px'; + elem.style.height = pos.height + 'px'; + elem.style.marginBottom = '-' + elem.style.height; } private updateCSSVariables() { - const variables = [ - 'quill.tooltip-remove', 'quill.tooltip-action-save', 'quill.tooltip-action', 'quill.tooltip-label', - 'quill.tooltip-label-link', 'quill.tooltip-label-image', 'quill.tooltip-label-video', - 'quill.tooltip-label-formula' - ]; + const variables = ['quill.tooltip-remove', 'quill.tooltip-action', 'quill.tooltip-label']; for (const variable of variables) { this.translateService.get(variable).subscribe(translation => { document.body.style.setProperty('--' + variable.replace('.', '-'), JSON.stringify(translation)); diff --git a/src/app/components/shared/write-comment/write-comment.marks.ts b/src/app/components/shared/view-comment-data/view-comment-data.marks.ts similarity index 84% rename from src/app/components/shared/write-comment/write-comment.marks.ts rename to src/app/components/shared/view-comment-data/view-comment-data.marks.ts index d9cef879a1a96ff689181aa49e5eecd11a8f1183..b2cca2120940d96c68b0ae8d9ce3626925e52008 100644 --- a/src/app/components/shared/write-comment/write-comment.marks.ts +++ b/src/app/components/shared/view-comment-data/view-comment-data.marks.ts @@ -1,5 +1,6 @@ import { LanguagetoolResult } from '../../../services/http/languagetool.service'; import { QuillEditorComponent } from 'ngx-quill'; +import { mark } from '@angular/compiler-cli/src/ngtsc/perf/src/clock'; class ContentIndexFinder { @@ -115,19 +116,12 @@ export class Marks { } buildErrors(initialText: string, res: LanguagetoolResult): void { + this.onClick(null); + this.clear(); const indexFinder = new ContentIndexFinder(this.editor.quillEditor.getContents().ops); - for (let i = 0; i < res.matches.length; i++) { - const match = res.matches[i]; + for (const match of res.matches) { const [start, len] = indexFinder.adjustTextIndexes(match.offset, match.length); - const mark = new Mark(start, len, this.markContainer, this.tooltipContainer, this.editor.quillEditor); - mark.setSuggestions(res, i, () => { - const index = this.textErrors.findIndex(elem => elem === mark); - if (index >= 0) { - this.textErrors.splice(index, 1); - } - mark.remove(); - }); - this.textErrors.push(mark); + this.createMark(start, len, match); } this.sync(); } @@ -139,10 +133,33 @@ export class Marks { error.syncMark(parentRect, editorRect.y - parentRect.y); } } + + copy(marks: Marks) { + this.onClick(null); + this.clear(); + for (const oldMark of marks.textErrors) { + this.createMark(oldMark.startIndex, oldMark.markLength, oldMark); + } + this.sync(); + } + + private createMark(start: number, len: number, dataObject: any) { + const newMark = new Mark(start, len, this.markContainer, this.tooltipContainer, this.editor.quillEditor); + newMark.setSuggestions(dataObject.replacements, dataObject.message, () => { + const index = this.textErrors.findIndex(elem => elem === newMark); + if (index >= 0) { + this.textErrors.splice(index, 1); + } + newMark.remove(); + }); + this.textErrors.push(newMark); + } } class Mark { + public replacements: { value?: string }[]; + public message: string; private marks: HTMLSpanElement[] = []; private dropdown: HTMLDivElement; @@ -165,12 +182,12 @@ class Mark { } this.marks.length = boundaries.length; for (let i = 0; i < this.marks.length; i++) { - const mark = this.marks[i]; + const current = this.marks[i]; const rect = this.quillEditor.getBounds(boundaries[i][0], boundaries[i][1]); - mark.style.setProperty('--width', rect.width + 'px'); - mark.style.setProperty('--height', rect.height + 'px'); - mark.style.setProperty('--left', rect.left + 'px'); - mark.style.setProperty('--top', (rect.top + offset) + 'px'); + current.style.setProperty('--width', rect.width + 'px'); + current.style.setProperty('--height', rect.height + 'px'); + current.style.setProperty('--left', rect.left + 'px'); + current.style.setProperty('--top', (rect.top + offset) + 'px'); } } @@ -200,21 +217,23 @@ class Mark { } remove() { - for (const mark of this.marks) { - mark.remove(); + for (const current of this.marks) { + current.remove(); } this.marks.length = 0; this.dropdown.remove(); } - setSuggestions(result: LanguagetoolResult, index: number, removeMark: () => void): void { + setSuggestions(replacements: { value?: string }[], message: string, removeMark: () => void): void { + this.replacements = replacements; + this.message = message; this.dropdown = document.createElement('div'); this.dropdown.classList.add('dropdownBlock'); - const suggestions = result.matches[index].replacements; + const suggestions = replacements; if (!suggestions.length) { const dropdownElem = document.createElement('span'); dropdownElem.classList.add('error-message'); - dropdownElem.append(result.matches[index].message); + dropdownElem.append(message); this.dropdown.append(dropdownElem); } else { const length = suggestions.length > 3 ? 3 : suggestions.length; diff --git a/src/app/components/shared/write-comment/write-comment.component.html b/src/app/components/shared/write-comment/write-comment.component.html index 32a9d67ce13b57558f363173015f13b35dd7050e..6caa9719121e6d2fad59369cb4236b9549ea7c8e 100644 --- a/src/app/components/shared/write-comment/write-comment.component.html +++ b/src/app/components/shared/write-comment/write-comment.component.html @@ -50,8 +50,7 @@ [isEditor]="true" [maxTextCharacters]="maxTextCharacters" [maxDataCharacters]="maxDataCharacters" - [placeHolderText]="placeholder" - [markEvents]="getMarkEvents()"></app-view-comment-data> + [placeHolderText]="placeholder"></app-view-comment-data> <ars-row ars-flex-box *ngIf="enabled" class="spellcheck"> <ars-col> <button diff --git a/src/app/components/shared/write-comment/write-comment.component.scss b/src/app/components/shared/write-comment/write-comment.component.scss index a54b280fe5f29f6cad4c4d64bf7a340970c1e1fb..cdbad181324268284e32a7c01f457f3a67271e5d 100644 --- a/src/app/components/shared/write-comment/write-comment.component.scss +++ b/src/app/components/shared/write-comment/write-comment.component.scss @@ -14,10 +14,9 @@ button { } .spell-button { - background-color: var(--primary); - color: var(--on-primary); + background-color: var(--secondary); + color: var(--on-secondary); margin-top: 1rem; - animation: shake 1.5s; } .spellcheck { 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 324be593f6953fb88b24dec562dd298a7351e268..c9d7b8d945b3b87d6517a8a6214ddc6ffa45d048 100644 --- a/src/app/components/shared/write-comment/write-comment.component.ts +++ b/src/app/components/shared/write-comment/write-comment.component.ts @@ -1,14 +1,14 @@ import { Component, ElementRef, Input, OnInit, TemplateRef, ViewChild } from '@angular/core'; import { TranslateService } from '@ngx-translate/core'; -import { Language, LanguagetoolService } from '../../../services/http/languagetool.service'; +import { Language, LanguagetoolResult, LanguagetoolService } from '../../../services/http/languagetool.service'; import { Comment } from '../../../models/comment'; import { NotificationService } from '../../../services/util/notification.service'; import { EventService } from '../../../services/util/event.service'; -import { Marks } from './write-comment.marks'; import { LanguageService } from '../../../services/util/language.service'; -import { QuillEditorComponent } from 'ngx-quill'; import { ViewCommentDataComponent } from '../view-comment-data/view-comment-data.component'; -import { DeepLService } from '../../../services/http/deep-l.service'; +import { DeepLService, SourceLang, TargetLang } from '../../../services/http/deep-l.service'; +import { DeepLDialogComponent } from '../_dialogs/deep-ldialog/deep-ldialog.component'; +import { MatDialog } from '@angular/material/dialog'; @Component({ selector: 'app-write-comment', @@ -42,15 +42,14 @@ export class WriteCommentComponent implements OnInit { isSpellchecking = false; hasSpellcheckConfidence = true; newLang = 'auto'; - // Marks - marks: Marks; constructor(private notification: NotificationService, private languageService: LanguageService, private translateService: TranslateService, public eventService: EventService, public languagetoolService: LanguagetoolService, - public deepl: DeepLService) { + private deeplService: DeepLService, + private dialog: MatDialog) { this.languageService.langEmitter.subscribe(lang => { this.translateService.use(lang); }); @@ -78,28 +77,18 @@ export class WriteCommentComponent implements OnInit { return undefined; } return () => { - if (this.checkInputData(this.commentData.currentData, this.commentData.currentText)) { + if (ViewCommentDataComponent.checkInputData(this.commentData.currentData, this.commentData.currentText, + this.translateService, this.notification, this.maxTextCharacters, this.maxDataCharacters)) { this.onSubmit(this.commentData.currentData, this.commentData.currentText, this.selectedTag); } }; } - onDocumentClick(e) { - if (!this.marks) { - return; - } - const range = this.commentData.editor.quillEditor.getSelection(false); - this.marks.onClick(range && range.length === 0 ? range.index : null); - } - checkGrammar() { this.grammarCheck(this.commentData.currentText, this.langSelect && this.langSelect.nativeElement); } grammarCheck(rawText: string, langSelect: HTMLSpanElement): void { - this.onDocumentClick({ - target: document - }); this.isSpellchecking = true; this.hasSpellcheckConfidence = true; this.checkSpellings(rawText).subscribe((wordsCheck) => { @@ -109,7 +98,7 @@ export class WriteCommentComponent implements OnInit { return; } if (this.selectedLang === 'auto' && - (langSelect.innerText.includes(this.newLang) || langSelect.innerText.includes('auto'))) { + (langSelect.innerText.includes(this.newLang) || langSelect.innerText.includes('auto'))) { if (wordsCheck.language.name.includes('German')) { this.selectedLang = 'de-DE'; } else if (wordsCheck.language.name.includes('English')) { @@ -121,10 +110,16 @@ export class WriteCommentComponent implements OnInit { } langSelect.innerHTML = this.newLang; } - this.marks.clear(); - this.marks.buildErrors(rawText, wordsCheck); - }, () => { - this.isSpellchecking = false; + const previous = this.commentData.currentData; + this.openDeeplDialog(previous, rawText, wordsCheck, + (data: string, text: string, view: ViewCommentDataComponent) => { + if (view === this.commentData) { + this.commentData.buildMarks(rawText, wordsCheck); + } else { + this.commentData.currentData = data; + this.commentData.copyMarks(view); + } + }); }, () => { this.isSpellchecking = false; }); @@ -138,40 +133,48 @@ export class WriteCommentComponent implements OnInit { return this.languagetoolService.checkSpellings(text, language); } - getMarkEvents() { - return { - onCreate: (markContainer: HTMLDivElement, tooltipContainer: HTMLDivElement, editor: QuillEditorComponent) => { - this.marks = new Marks(markContainer, tooltipContainer, editor); - }, - onChange: (delta: any) => { - this.marks.onDataChange(delta); - }, - onEditorChange: () => { - this.marks.sync(); - }, - onDocumentClick: (e) => this.onDocumentClick(e) - }; - } - - private checkInputData(data: string, text: string): boolean { - text = text.trim(); - if (!text.length) { - this.translateService.get('comment-page.error-comment').subscribe(message => { - this.notification.show(message); - }); - return false; - } else if (text.length > this.maxTextCharacters) { - this.translateService.get('comment-page.error-comment-text').subscribe(message => { - this.notification.show(message); - }); - return false; - } else if (data.length > this.maxDataCharacters) { - this.translateService.get('comment-page.error-comment-data').subscribe(message => { - this.notification.show(message); - }); - return false; + private openDeeplDialog(body: string, + text: string, + result: LanguagetoolResult, + onClose: (data: string, text: string, view: ViewCommentDataComponent) => void) { + let target = TargetLang.EN_US; + const code = result.language.detectedLanguage.code.toUpperCase().split('-')[0]; + const source = code in SourceLang ? SourceLang[code] : null; + if (code.startsWith(SourceLang.EN)) { + target = TargetLang.DE; } - return true; + DeepLDialogComponent.generateDeeplDelta(this.deeplService, body, target) + .subscribe(([improvedBody, improvedText]) => { + this.isSpellchecking = false; + if (improvedText.replace(/\s+/g, '') === text.replace(/\s+/g, '')) { + onClose(body, text, this.commentData); + return; + } + this.dialog.open(DeepLDialogComponent, { + width: '900px', + maxWidth: '100%', + data: { + body, + text, + improvedBody, + improvedText, + maxTextCharacters: this.maxTextCharacters, + maxDataCharacters: this.maxDataCharacters, + isModerator: this.isModerator, + result, + onClose, + target: DeepLService.transformSourceToTarget(source), + usedTarget: target + } + }).afterClosed().subscribe((val) => { + if (val) { + this.buildCreateCommentActionCallback()(); + } + }); + }, (_) => { + this.isSpellchecking = false; + onClose(body, text, this.commentData); + }); } } diff --git a/src/app/directives/joyride-template.directive.ts b/src/app/directives/joyride-template.directive.ts index 78b18a019cc10ce6d24ddeff70a8ae003d94af00..63507799fa8f915279d52ae69ce8b3b477c87ed5 100644 --- a/src/app/directives/joyride-template.directive.ts +++ b/src/app/directives/joyride-template.directive.ts @@ -1,7 +1,6 @@ import { ComponentFactoryResolver, Directive, - Input, OnInit, ViewContainerRef } from '@angular/core'; diff --git a/src/app/services/http/deep-l.service.ts b/src/app/services/http/deep-l.service.ts index 7125235d5dee9ab6e2435d9993df5de8e8f00ee5..499c71f22726463fa3735f9ea040cd6dfcba1168 100644 --- a/src/app/services/http/deep-l.service.ts +++ b/src/app/services/http/deep-l.service.ts @@ -2,7 +2,7 @@ import { Injectable } from '@angular/core'; import { BaseHttpService } from './base-http.service'; import { HttpClient, HttpHeaders } from '@angular/common/http'; import { Observable } from 'rxjs'; -import { catchError, map, tap } from 'rxjs/operators'; +import { catchError, map, tap, timeout } from 'rxjs/operators'; import { flatMap } from 'rxjs/internal/operators'; const httpOptions = { @@ -12,11 +12,74 @@ const httpOptions = { interface DeepLResult { translations: { - detected_source_language: string; + // eslint-disable-next-line @typescript-eslint/naming-convention + detected_source_language: SourceLang; text: string; }[]; } +export enum SourceLang { + BG = 'BG', + CS = 'CS', + DA = 'DA', + DE = 'DE', + EL = 'EL', + EN = 'EN', + ES = 'ES', + ET = 'ET', + FI = 'FI', + FR = 'FR', + HU = 'HU', + IT = 'IT', + JA = 'JA', + LT = 'LT', + LV = 'LV', + NL = 'NL', + PL = 'PL', + PT = 'PT', + RO = 'RO', + RU = 'RU', + SK = 'SK', + SL = 'SL', + SV = 'SV', + ZH = 'ZH' +} + +export enum TargetLang { + BG = 'BG', + CS = 'CS', + DA = 'DA', + DE = 'DE', + EL = 'EL', + EN_GB = 'EN-GB', + EN_US = 'EN-US', + ES = 'ES', + ET = 'ET', + FI = 'FI', + FR = 'FR', + HU = 'HU', + IT = 'IT', + JA = 'JA', + LT = 'LT', + LV = 'LV', + NL = 'NL', + PL = 'PL', + PT_PT = 'PT-PT', + PT_BR = 'PT-BR', + RO = 'RO', + RU = 'RU', + SK = 'SK', + SL = 'SL', + SV = 'SV', + ZH = 'ZH' +} + +export enum FormalityType { + default = '', + less = 'less', + more = 'more' +} + @Injectable({ providedIn: 'root' }) @@ -26,24 +89,57 @@ export class DeepLService extends BaseHttpService { super(); } - improveTextStyle(text: string): Observable<string> { - return this.makeTranslateRequest([text], 'EN-US').pipe( + public static transformSourceToTarget(lang: SourceLang): TargetLang { + switch (lang) { + case SourceLang.EN: + return TargetLang.EN_US; + case SourceLang.PT: + return TargetLang.PT_PT; + default: + return TargetLang[lang]; + } + } + + public static supportsFormality(lang: TargetLang): boolean { + switch (lang) { + case TargetLang.DE: + case TargetLang.ES: + case TargetLang.FR: + case TargetLang.IT: + case TargetLang.NL: + case TargetLang.PL: + case TargetLang.PT_BR: + case TargetLang.PT_PT: + case TargetLang.RU: + return true; + default: + return false; + } + } + + improveTextStyle(text: string, temTargetLang: TargetLang, formality = FormalityType.default): Observable<string> { + return this.makeXMLTranslateRequest(text, temTargetLang, formality).pipe( flatMap(result => - this.makeTranslateRequest([result.translations[0].text], result.translations[0].detected_source_language)), + this.makeXMLTranslateRequest( + result.translations[0].text, + DeepLService.transformSourceToTarget(result.translations[0].detected_source_language), + formality + )), map(result => result.translations[0].text) ); } - private makeTranslateRequest(text: string[], targetLang: string): Observable<DeepLResult> { + private makeXMLTranslateRequest(text: string, targetLang: TargetLang, formality: FormalityType): Observable<DeepLResult> { const url = '/deepl/translate'; - console.assert(text.length > 0, 'You need at least one text entry.'); - console.assert(text.length <= 50, 'Maximum 50 text entries are allowed'); + const tagFormality = DeepLService.supportsFormality(targetLang) && formality !== FormalityType.default ? '&formality=' + formality : ''; const additional = '?target_lang=' + encodeURIComponent(targetLang) + - '&text=' + text.map(e => encodeURIComponent(e)).join('&text='); + '&tag_handling=xml' + tagFormality + + '&text=' + encodeURIComponent(text); return this.http.get<string>(url + additional, httpOptions) .pipe( tap(_ => ''), - catchError(this.handleError<any>('makeTranslateRequest')), + timeout(5000), + catchError(this.handleError<any>('makeXMLTranslateRequest')), ); } } diff --git a/src/app/services/util/tag-cloud-data.service.ts b/src/app/services/util/tag-cloud-data.service.ts index 0b35e602dee1ece68131bc5daaf0295936d24dfc..2e4e038f25ce664a4ef8b47e965ef12406a151c2 100644 --- a/src/app/services/util/tag-cloud-data.service.ts +++ b/src/app/services/util/tag-cloud-data.service.ts @@ -23,6 +23,9 @@ export interface TagCloudDataTagEntry { categories: Set<string>; dependencies: Set<string>; comments: Comment[]; + generatedByQuestionerCount: number; + taggedCommentsCount: number; + answeredCommentsCount: number; } export interface TagCloudMetaData { @@ -99,43 +102,50 @@ export class TagCloudDataService { const data: TagCloudData = new Map<string, TagCloudDataTagEntry>(); const users = new Set<number>(); for (const comment of comments) { - TopicCloudAdminService.approveKeywordsOfComment(comment, adminData, (keyword: SpacyKeyword) => { - let current: TagCloudDataTagEntry = data.get(keyword.text); - const commentDate = new Date(comment.timestamp); - if (current === undefined) { - current = { - cachedVoteCount: 0, - cachedUpVotes: 0, - cachedDownVotes: 0, - comments: [], - weight: 0, - adjustedWeight: 0, - distinctUsers: new Set<number>(), - categories: new Set<string>(), - dependencies: new Set<string>([...keyword.dep]), - firstTimeStamp: commentDate, - lastTimeStamp: commentDate - }; - data.set(keyword.text, current); - } - keyword.dep.forEach(dependency => current.dependencies.add(dependency)); - current.cachedVoteCount += comment.score; - current.cachedUpVotes += comment.upvotes; - current.cachedDownVotes += comment.downvotes; - current.distinctUsers.add(comment.userNumber); - if (comment.tag) { - current.categories.add(comment.tag); - } - // @ts-ignore - if (current.firstTimeStamp - commentDate > 0) { - current.firstTimeStamp = commentDate; - } - // @ts-ignore - if (current.lastTimeStamp - commentDate < 0) { - current.lastTimeStamp = commentDate; - } - current.comments.push(comment); - }); + TopicCloudAdminService.approveKeywordsOfComment(comment, adminData, + (keyword: SpacyKeyword, isFromQuestioner: boolean) => { + let current: TagCloudDataTagEntry = data.get(keyword.text); + const commentDate = new Date(comment.timestamp); + if (current === undefined) { + current = { + cachedVoteCount: 0, + cachedUpVotes: 0, + cachedDownVotes: 0, + comments: [], + weight: 0, + adjustedWeight: 0, + distinctUsers: new Set<number>(), + categories: new Set<string>(), + dependencies: new Set<string>([...keyword.dep]), + firstTimeStamp: commentDate, + lastTimeStamp: commentDate, + generatedByQuestionerCount: 0, + taggedCommentsCount: 0, + answeredCommentsCount: 0 + }; + data.set(keyword.text, current); + } + keyword.dep.forEach(dependency => current.dependencies.add(dependency)); + current.cachedVoteCount += comment.score; + current.cachedUpVotes += comment.upvotes; + current.cachedDownVotes += comment.downvotes; + current.distinctUsers.add(comment.userNumber); + current.generatedByQuestionerCount += +isFromQuestioner; + current.taggedCommentsCount += +!!comment.tag; + current.answeredCommentsCount += +!!comment.answer; + if (comment.tag) { + current.categories.add(comment.tag); + } + // @ts-ignore + if (current.firstTimeStamp - commentDate > 0) { + current.firstTimeStamp = commentDate; + } + // @ts-ignore + if (current.lastTimeStamp - commentDate < 0) { + current.lastTimeStamp = commentDate; + } + current.comments.push(comment); + }); users.add(comment.userNumber); } return [ @@ -199,7 +209,10 @@ export class TagCloudDataService { distinctUsers: new Set<number>(), dependencies: new Set<string>(), firstTimeStamp: new Date(), - lastTimeStamp: new Date() + lastTimeStamp: new Date(), + generatedByQuestionerCount: 0, + taggedCommentsCount: 0, + answeredCommentsCount: 0 }); } }); @@ -317,13 +330,19 @@ export class TagCloudDataService { } private calculateWeight(tagData: TagCloudDataTagEntry): number { + const value = Math.max(tagData.cachedVoteCount, 0); + const additional = (tagData.distinctUsers.size - 1) * 0.5 + + tagData.comments.reduce((acc, comment) => acc + +!!comment.createdFromLecturer, 0) + + tagData.generatedByQuestionerCount + + tagData.taggedCommentsCount + + tagData.answeredCommentsCount; switch (this._calcWeightType) { case TagCloudCalcWeightType.byVotes: - return tagData.cachedVoteCount; + return value + additional; case TagCloudCalcWeightType.byLengthAndVotes: - return tagData.cachedVoteCount / 10.0 + tagData.comments.length; + return value / 10.0 + tagData.comments.length + additional; default: - return tagData.comments.length; + return tagData.comments.length + additional; } } diff --git a/src/app/services/util/topic-cloud-admin.service.ts b/src/app/services/util/topic-cloud-admin.service.ts index efbdbaa4189a0660b36ee04d53975508f56f2275..2da5fb97bc375ba1cfa369d6c93b5181c407734d 100644 --- a/src/app/services/util/topic-cloud-admin.service.ts +++ b/src/app/services/util/topic-cloud-admin.service.ts @@ -52,11 +52,16 @@ export class TopicCloudAdminService { room.tagCloudSettings = JSON.stringify(settings); } - static approveKeywordsOfComment(comment: Comment, config: TopicCloudAdminData, keywordFunc: (SpacyKeyword) => void) { + static approveKeywordsOfComment(comment: Comment, config: TopicCloudAdminData, keywordFunc: (SpacyKeyword, boolean) => void) { let source = comment.keywordsFromQuestioner; + let isFromQuestioner = true; if (config.keywordORfulltext === KeywordOrFulltext.both) { - source = !source || !source.length ? comment.keywordsFromSpacy : source; + if (!source || !source.length) { + source = comment.keywordsFromSpacy; + isFromQuestioner = false; + } } else if (config.keywordORfulltext === KeywordOrFulltext.fulltext) { + isFromQuestioner = false; source = comment.keywordsFromSpacy; } if (!source) { @@ -76,7 +81,7 @@ export class TopicCloudAdminService { } } if (!isProfanity) { - keywordFunc(keyword); + keywordFunc(keyword, isFromQuestioner); } } } diff --git a/src/app/utils/cloud-parameters.ts b/src/app/utils/cloud-parameters.ts index 751bd0b2eef276658caf730bca3f1cd8303fffbb..6d874fc3b548f37e8d697139894d4af395957a19 100644 --- a/src/app/utils/cloud-parameters.ts +++ b/src/app/utils/cloud-parameters.ts @@ -137,10 +137,10 @@ export class CloudParameters { const minValue = window.innerWidth < window.innerHeight ? window.innerWidth : window.innerHeight; const isMobile = minValue < 700; const elements = isMobile ? 7 : 10; - this.fontFamily = 'Dancing Script'; + this.fontFamily = 'sans-serif'; this.fontStyle = 'normal'; this.fontWeight = 'normal'; - this.fontSize = '14px'; + this.fontSize = '16px'; this.backgroundColor = CloudParameters.resolveColor(p, theme.backgroundColor); this.fontColor = CloudParameters.resolveColor(p, theme.hoverColor); this.fontSizeMin = CloudParameters.mapValue(minValue, 375, 750, 125, 200); diff --git a/src/assets/i18n/creator/de.json b/src/assets/i18n/creator/de.json index 0be7af5633a03d9f2bb94b12878d6e1e6556759d..8a933c1a149a457e48be2ab53d61c64a7c1a3b2e 100644 --- a/src/assets/i18n/creator/de.json +++ b/src/assets/i18n/creator/de.json @@ -95,7 +95,7 @@ "editing-done-hint": "Editieren beenden", "force-language-selection": "Die Sprache der Eingabe konnte nicht automatisch erkannt werden.", "add-manually": "Gib ein Stichwort zu deiner Frage ein. Trenne mehrere Stichwörter mit einem Komma.", - "select-keywords": "Die Textanalyse schlägt folgende Stichwörter vor. Welche kennzeichnen deine Frage am besten?" + "select-keywords": "Welche Stichwörter beschreiben deine Frage am besten?" }, "comment-page": { "a11y-comment_delete": "Löscht diese Frage", @@ -191,7 +191,7 @@ "edit-favorite-reset": "Markierung zurücksetzen", "edit-bookmark": "Lesezeichen setzen", "edit-bookmark-reset": "Markierung zurücksetzen", - "grammar-check": "Rechtschreibprüfung", + "grammar-check": "Text prüfen", "show-comment-with-filter": "Vulgäre Wörter ausixen", "show-comment-without-filter": "Vulgäre Wörter anzeigen", "upvote": "positiv", @@ -242,6 +242,20 @@ "option-normal": "Deine Eingabe:", "option-improved": "KI-Vorschlag:" }, + "deepl-formality-select": { + "error": "Der Text konnte nicht aktualisiert werden.", + "name": "Stil", + "default": "Automatisch", + "less": "Informell", + "more": "Formell" + }, + "explanation": { + "label": "Warum?", + "close": "Schließen", + "deepl": "## Text optimieren \n\nUm deine Frage optimal lesbar und verständlich zu präsentieren, lassen wir sie mit dem KI-Übersetzungsprogramm [DeepL](https://www.deepl.com/translator) ins Englische und zurück ins Deutsche übersetzen. \n\nDie Rückübersetzung ist in fast allen Fällen besser als das Original in Bezug auf Rechtschreibung, Grammatik, Interpunktion und Sprachstil.", + "spacy": "## Stichwörter \n\nMittels NLP (Natural Language Processing) wird deine Frage grammatikalisch analysiert. Die erkannten Substantive werden in ihre Grundform gebracht, d. h. lemmatisiert, und dir als Stichwörter vorgeschlagen. Für die Textanalyse verwenden wir die freie NLP-Software [spaCy](https://spacy.io/). \n\nDie Stichwörter können verwendet werden, um die Liste der Fragen zu filtern oder um eine Wortwolke zu erstellen.", + "topic-cloud": "## Themen als Wortwolke \n\nUnsere **Themenwolke** visualisiert die Häufigkeit der Stichwörter: Je größer die Schrift, desto mehr Fragen beziehen sich auf das Stichwort. Die Bewertung der Fragen geht in die Schriftgröße mit ein. \n\nDie Themenwolke dient als **Navigator** zu allen Fragen zu einem Stichwort: Wenn du auf ein Wort in der Wolke klickst, gelangst du zu den Fragen mit diesem Stichwort." + }, "home-page": { "create-session": "Neue Sitzung", "created-1": "Die Sitzung »", @@ -249,6 +263,8 @@ "no-empty-name": "Gib einen Namen ein. Der Raum-Code wird generiert." }, "quill": { + "cancel": "Abbrechen", + "heading": "Eingabe", "tooltip-remove": "Löschen", "tooltip-action-save": "Speichern", "tooltip-action": "Editieren", @@ -292,7 +308,7 @@ "changes-successful": "Änderungen gespeichert.", "comments": "Öffentliche Fragen", "comments-deleted": "Alle Fragen wurden gelöscht.", - "copy-session-id": "Kopiert den Link zu diesem Raum in die Zwischenablage.", + "copy-session-id": "Gib den Link zu diesem Raum an die Teilnehmer weiter.", "create-content": "Frage stellen", "default-content-group": "Standard", "delete-all-comments": "Fragen löschen", @@ -326,7 +342,7 @@ "reallyContent": "Willst du die Frage ", "reallySession": "Willst du die Sitzung ", "room-not-found": "Sitzung wurde nicht gefunden (", - "session-id": "Raum", + "session-id": "Raum-Code", "session-id-copied": "Direktlink wurde in die Zwischenablage kopiert.", "session-settings": "Sitzungsverwaltung", "session-question-board": "Zur öffentlichen Fragenliste", @@ -543,5 +559,13 @@ "valid": "VALID", "invalid": "INVALID", "cant-find-comment": "Die Frage kann nicht gefunden werden" + }, + "worker-dialog": { + "running": "Laufend", + "room-name" : "Raum", + "comments" : "Abgearbeitete Fragen", + "bad-spelled" : "Zu schlechte Rechtschreibung", + "failed" : "Fehler aufgetreten", + "inline-header": "Laufende Stichwort-Aktualisierungen" } } diff --git a/src/assets/i18n/creator/en.json b/src/assets/i18n/creator/en.json index eebfc600d65992a831323ebe535c0663522c1be0..6a1503476010da25738d58494de2bfa574359645 100644 --- a/src/assets/i18n/creator/en.json +++ b/src/assets/i18n/creator/en.json @@ -96,7 +96,7 @@ "editing-done-hint": "Finish editing", "force-language-selection": "The language of the text input could not be detected automatically.", "add-manually": "Enter a keyword for your question. Separate several keywords with a comma.", - "select-keywords": "The text analysis suggests the following keywords. Which best characterise your question?" + "select-keywords": "The text analysis suggests keywords. Which best describe your question?" }, "comment-page": { "a11y-comment_delete": "Deletes this question", @@ -192,7 +192,7 @@ "edit-favorite-reset": "Remove marker", "edit-bookmark": "Bookmark", "edit-bookmark-reset": "Remove marker", - "grammar-check": "Spell check", + "grammar-check": "Check text", "show-comment-with-filter": "Hide vulgar words", "show-comment-without-filter": "Show vulgar words", "upvote": "upvotes", @@ -243,6 +243,20 @@ "option-normal": "Your input:", "option-improved": "AI suggestion:" }, + "deepl-formality-select": { + "error": "The text could not be updated.", + "name": "Style", + "default": "Automatic", + "less": "Informal", + "more": "Formal" + }, + "explanation": { + "label": "Explanation", + "close": "Close", + "deepl": "## Text optimization\n\nTo make your question as readable and understandable as possible, we have it translated into German and back into English using the translation program [DeepL](https://www.deepl.com/translator).\n\nThe back translation is in almost all cases better than the original in terms of spelling, grammar, punctuation and language style.", + "spacy": "## Text analysis\n\nUsing NLP (Natural Language Processing) your question will be analyzed grammatically. The recognized nouns are put into their basic form, i.e. lemmatized, and suggested to you as keywords. For the text analysis we use the free NLP software [spaCy](https://spacy.io/). \n\nThe keywords can be used to filter the list of questions or to create a word cloud.", + "topic-cloud": "## Topic cloud\n\nThe topic cloud visualizes the frequency of the keywords: the larger the font, the more questions refer to the keyword. The rating of the questions is included in the font size.\n\nThe topic cloud serves as a navigator to all questions related to a selected keyword: If you click on a word in the cloud, you will get to the questions related to this keyword." + }, "home-page": { "create-session": "New session", "created-1": "Session »", @@ -250,6 +264,8 @@ "no-empty-name": "Please enter a name." }, "quill": { + "cancel": "Cancel", + "heading": "Input", "tooltip-remove": "Delete", "tooltip-action-save": "Save", "tooltip-action": "Edit", @@ -293,7 +309,7 @@ "changes-successful": "Successfully updated.", "comments": "Public questions", "comments-deleted": "All questions have been deleted.", - "copy-session-id": "Copy the link to this session to the clipboard and pass it on to the session participants.", + "copy-session-id": "Share the link to this room with your participants.", "create-content": "Create content", "default-content-group": "Default", "delete-all-comments": "Delete questions", @@ -327,7 +343,7 @@ "reallyContent": "Do you really want to delete content ", "reallySession": "Do you really want to delete session ", "room-not-found": "Session not found :(", - "session-id": "Key", + "session-id": "Key code", "session-id-copied": "Session link was copied to the clipboard.", "session-settings": "Session administration", "session-question-board": "To the public question list", @@ -541,5 +557,13 @@ "valid": "VALID", "invalid": "INVALID", "cant-find-comment": "Can't find comment" + }, + "worker-dialog": { + "running": "Running", + "room-name": "Session name", + "comments": "Questions", + "bad-spelled": "Spelling too bad", + "failed": "Error occurred", + "inline-header": "Ongoing keyword updates" } } diff --git a/src/assets/i18n/demo/demo-de.html b/src/assets/i18n/demo/demo-de.html index 908c88f483dbd4141f27a990f9408f1c2fa48ebe..d334377a3e62879ecdbc3558cbe497ec2c855921 100644 --- a/src/assets/i18n/demo/demo-de.html +++ b/src/assets/i18n/demo/demo-de.html @@ -90,7 +90,7 @@ Sie visualisiert die Häufigkeit der Stichwörter: Je größer die Schrift, desto mehr Fragen beziehen sich auf das Stichwort. Auch die Bewertung der Fragen geht in die Schriftgröße mit ein. - Die Themenwolke fungiert zugleich als Navigator zu allen Fragen eines ausgewählten Stichwortes: + Die Themenwolke fungiert zugleich als Navigator zu allen Fragen eines Stichwortes: Klickt man auf ein Wort in der Wolke, gelangt man zu den Fragen mit diesem Stichwort. </p> diff --git a/src/assets/i18n/home/de.json b/src/assets/i18n/home/de.json index a1183eb18840c3d97f0e535a41e2bbfbb6038bb3..77d9300d0a3701f27c9bbd67c84aefea7fbb5fa5 100644 --- a/src/assets/i18n/home/de.json +++ b/src/assets/i18n/home/de.json @@ -57,7 +57,14 @@ "tag-cloud-questions-current-filtered": "Fragenliste", "tag-cloud-questions-brainstorming": "Fragen, die ab jetzt gestellt werden (Brainstorming)", "tag-cloud-questions-brainstorming-short": "Brainstorming", - "tag-cloud-create": "Anzeigen" + "tag-cloud-create": "Weiter" + }, + "explanation": { + "label": "Warum?", + "close": "Schließen", + "deepl": "## Text optimieren \n\nUm deine Frage optimal lesbar und verständlich zu präsentieren, lassen wir sie mit dem KI-Übersetzungsprogramm [DeepL](https://www.deepl.com/translator) ins Englische und zurück ins Deutsche übersetzen. \n\nDie Rückübersetzung ist in fast allen Fällen besser als das Original in Bezug auf Rechtschreibung, Grammatik, Interpunktion und Sprachstil.", + "spacy": "## Stichwörter \n\nMittels NLP (Natural Language Processing) wird deine Frage grammatikalisch analysiert. Die erkannten Substantive werden in ihre Grundform gebracht, d. h. lemmatisiert, und dir als Stichwörter vorgeschlagen. Für die Textanalyse verwenden wir die freie NLP-Software [spaCy](https://spacy.io/). \n\nDie Stichwörter können verwendet werden, um die Liste der Fragen zu filtern oder um eine Wortwolke zu erstellen.", + "topic-cloud": "## Themen als Wortwolke \n\nUnsere **Themenwolke** visualisiert die Häufigkeit der Stichwörter: Je größer die Schrift, desto mehr Fragen beziehen sich auf das Stichwort. Die Bewertung der Fragen geht in die Schriftgröße mit ein. \n\nDie Themenwolke dient als **Navigator** zu allen Fragen zu einem Stichwort: Wenn du auf ein Wort in der Wolke klickst, gelangst du zu den Fragen mit diesem Stichwort." }, "header": { "abort": "Abbrechen", @@ -85,7 +92,7 @@ "my-sessions": "Zu den Räumen", "really-delete-account": "Willst du dein Konto mit allen Sitzungen unwiderruflich löschen? Falls du Boni vergeben hast (Sterne), exportiere die Fragen, damit du eingereichte Bonus-Tokens überprüfen kannst.", "sure": "Bist du sicher?", - "user-bonus-token": "Bonus-Sterne", + "user-bonus-token": "Meine Bonus-Sterne", "user-got-tokens": "Du hast noch Sterne für Bonuspunkte, die verloren gehen!", "users-online": "Aktuell eingeloggte User", "visited-sessions": "Sitzungen", @@ -297,7 +304,7 @@ "panel-join-button": "", "panel-remove-button": "Eintrag entfernen", "delete-room": "Raum löschen", - "panel-session-id": "Raum", + "panel-session-id": "Raum-Code", "panel-session-name": "Veranstaltung", "panel-user-role": "Rolle", "participant-role": "Du bist Teilnehmer*in in diesem Raum.", @@ -354,7 +361,7 @@ "please-choose": "Bitte wähle zuerst eine Sitzung aus!" }, "qr-dialog": { - "session": "Raum" + "session": "Raum-Code" }, "topic-cloud": { "changes-gone-wrong": "Etwas ist schiefgelaufen!", diff --git a/src/assets/i18n/home/en.json b/src/assets/i18n/home/en.json index fa6f5895ad9a451358d3eca137f620fdd38f6271..2bb8b409ac532535b0f576fa0c4804b028baafbf 100644 --- a/src/assets/i18n/home/en.json +++ b/src/assets/i18n/home/en.json @@ -46,6 +46,13 @@ "motd-title-new": "Latest", "motd-mark-read": "Mark as read" }, + "explanation": { + "label": "Explain", + "close": "Close", + "deepl": "## Text optimization\n\nTo make your question as readable and understandable as possible, we have it translated into English and back into German using the translation program [DeepL] (https://www.deepl.com/translator). \n\nThe back translation is in almost all cases better than the original in terms of spelling, grammar, punctuation and language style.", + "spacy": "## Text analysis\n\nUsing NLP (Natural Language Processing) your question will be analyzed grammatically. The recognized nouns are put into their basic form, i.e. lemmatized, and suggested to you as keywords. For the text analysis we use the free NLP software [spaCy] (https://spacy.io/). \n\nThe keywords can be used to filter the list of questions or to create a word cloud.", + "topic-cloud": "## Topic cloud\n\nThe topic cloud visualizes the frequency of the keywords: the larger the font, the more questions refer to the keyword. The rating of the questions is included in the font size. \n\nThe topic cloud serves as a navigator to all questions related to a selected keyword: If you click on a word in the cloud, you will get to the questions related to this keyword." + }, "header": { "abort": "Cancel", "accessibility-back": "Go back to the previous page", @@ -72,7 +79,7 @@ "my-sessions": "Room list", "really-delete-account": "Do you want to irrevocably delete your account with all sessions? If you have given bonus stars, export the questions so that you can check submitted bonus tokens.", "sure": "Are you sure?", - "user-bonus-token": "Bonus stars", + "user-bonus-token": "My bonus stars", "user-got-tokens": "You haven't received a star for a good question yet.", "users-online": "Currently logged-in users", "visited-sessions": "Sessions", @@ -104,7 +111,7 @@ "overview-question-tooltip": "Number of questions", "overview-questioners-tooltip": "Number of questioners", "overview-keywords-tooltip": "Number of Keywords", - "update-spacy-keywords": "Add keywords", + "update-spacy-keywords": "Analyze questions", "overview-admin-config-enabled": "Themes requirement active", "quiz-now": "Quizzing", "moderation-warning": "Moderation board with unreleased questions", @@ -149,11 +156,11 @@ "tag-cloud-info": "The word cloud in »frag.jetzt« serves as a semantic filter: the larger the font, the more often the word was used grammatically in the questions or assigned as a keyword. The ratings of the questions also influence the font size.", "tag-cloud-questions-title": "Which questions should be included?", "tag-cloud-questions-all": "All questions", - "tag-cloud-questions-all-short": "All questions", + "tag-cloud-questions-all-short": "All", "tag-cloud-questions-current-filtered": "Question list", - "tag-cloud-questions-brainstorming": "Brain storming session: questions that will be asked from now on", + "tag-cloud-questions-brainstorming": "Brain storming session: questions from now on", "tag-cloud-questions-brainstorming-short": "Brain storming session", - "tag-cloud-create": "Create word cloud" + "tag-cloud-create": "Create" }, "imprint": { "cancel": "Close", diff --git a/src/assets/i18n/participant/de.json b/src/assets/i18n/participant/de.json index d7bdaae5e5e0e103cd5dc5a0b3ddbeb45d02aab2..ace40fb256a2c0144184fe1085ccca8c5fce5ae1 100644 --- a/src/assets/i18n/participant/de.json +++ b/src/assets/i18n/participant/de.json @@ -101,7 +101,7 @@ "editing-done-hint": "Editieren beenden", "force-language-selection": "Die Sprache der Eingabe konnte nicht automatisch erkannt werden.", "add-manually": "Gib ein Stichwort zu deiner Frage ein. Trenne mehrere Stichwörter mit einem Komma.", - "select-keywords": "Die Textanalyse schlägt folgende Stichwörter vor. Welche kennzeichnen deine Frage am besten?" + "select-keywords": "Welche Stichwörter beschreiben deine Frage am besten?" }, "comment-page": { "a11y-comment_input": "Gib deine Frage ein", @@ -160,7 +160,7 @@ "show-more": "Mehr ansehen", "show-less": "Weniger anzeigen", "sure": "Bist du sicher?", - "grammar-check": "Rechtschreibprüfung", + "grammar-check": "Text prüfen", "upvote": "positiv", "downvote": "negativ" }, @@ -170,6 +170,20 @@ "option-normal": "Deine Eingabe:", "option-improved": "KI-Vorschlag:" }, + "deepl-formality-select": { + "error": "Der Text konnte nicht aktualisiert werden.", + "name": "Stil", + "default": "Automatisch", + "less": "Informell", + "more": "Formell" + }, + "explanation": { + "label": "Warum?", + "close": "Schließen", + "deepl": "## Text optimieren \n\nUm deine Frage optimal lesbar und verständlich zu präsentieren, lassen wir sie mit dem KI-Übersetzungsprogramm [DeepL](https://www.deepl.com/translator) ins Englische und zurück ins Deutsche übersetzen. \n\nDie Rückübersetzung ist in fast allen Fällen besser als das Original in Bezug auf Rechtschreibung, Grammatik, Interpunktion und Sprachstil.", + "spacy": "## Stichwörter \n\nMittels NLP (Natural Language Processing) wird deine Frage grammatikalisch analysiert. Die erkannten Substantive werden in ihre Grundform gebracht, d. h. lemmatisiert, und dir als Stichwörter vorgeschlagen. Für die Textanalyse verwenden wir die freie NLP-Software [spaCy](https://spacy.io/). \n\nDie Stichwörter können verwendet werden, um die Liste der Fragen zu filtern oder um eine Wortwolke zu erstellen.", + "topic-cloud": "## Themen als Wortwolke \n\nUnsere **Themenwolke** visualisiert die Häufigkeit der Stichwörter: Je größer die Schrift, desto mehr Fragen beziehen sich auf das Stichwort. Die Bewertung der Fragen geht in die Schriftgröße mit ein. \n\nDie Themenwolke dient als **Navigator** zu allen Fragen zu einem Stichwort: Wenn du auf ein Wort in der Wolke klickst, gelangst du zu den Fragen mit diesem Stichwort." + }, "home-page": { "exactly-8": "Ein Raum-Code hat genau 8 Ziffern.", "no-room-found": "Es wurde kein Raum mit diesem Raum-Code gefunden.", @@ -194,14 +208,14 @@ "a11y-announcer": "Du befindest dich nun in der Sitzung mit dem von dir eingegebenen Raum-Code.", "a11y-question_answer": "Öffnet die Fragen-Seite und bietet dir die Möglichkeit, Fragen zu stellen.", "comments": "Fragen", - "create-comment": "Stell deine Frage!", + "create-comment": "Stell deine Fragen!", "default-content-group": "Standard", "description": "Beschreibung der Sitzung", "give-feedback": "Feedback geben", "learn": "Lernen", "live-announcer": "Du befindest dich jetzt in der Sitzung. Um Informationen zu Tastenkombinationen zu erhalten drücke jetzt die Enter-Taste oder rufe die Ansage zu einem späteren Zeitpunkt mit der Escape-Taste auf.", "live-feedback": "Live Feedback", - "session-id": "Raum", + "session-id": "Raum-Code", "bonus-token": "Tokens für Bonuspunkte", "bonus-token-header": "Tokens für Bonuspunkte", "delete": "Frage löschen", @@ -259,6 +273,8 @@ "questions-blocked": "Neue Fragen deaktiviert " }, "quill": { + "cancel": "Abbrechen", + "heading": "Eingabe", "tooltip-remove": "Löschen", "tooltip-action-save": "Speichern", "tooltip-action": "Editieren", @@ -276,7 +292,7 @@ "demo-data-topic": "Thema %d", "overview-question-topic-tooltip": "Anzahl Fragen mit diesem Thema", "overview-questioners-topic-tooltip": "Anzahl Fragensteller*innen mit diesem Thema", - "period-since-first-comment":"Zeitraum seit der ersten Frage", + "period-since-first-comment": "Zeitraum seit der ersten Frage", "upvote-topic": "Up-Votes für dieses Thema", "downvote-topic": "Down-Votes für dieses Thema", "blacklist-topic": "Thema auf die »Blacklist« setzen", @@ -408,12 +424,12 @@ "highestWeight-tooltip": "x Themen mit der höchsten Gewichtung anzeigen", "rotate-weight": "Themen dieser Häufigkeitsgruppe um x Grad drehen", "rotate-weight-tooltip": "Themen dieser Häufigkeitsgruppe um x Grad drehen", - "font":"Schrift", + "font": "Schrift", "reset-btn": "Auf Standardwerte setzen", "font-family-tooltip": "Schrift auswählen …", "bold-notation-tooltip": "Schrift fett setzen", - "font-style-bold" : "Fette Schrift", - "font-family":"Schriftart", + "font-style-bold": "Fette Schrift", + "font-family": "Schriftart", "manual-weight-number": "Anzahl Themen beschränken", "manual-weight-number-tooltip": "Anzahl Themen der Häufigkeitsgruppe", "manual-weight-number-note": "Begrenzt die Anzahl Themen einer Häufigkeitsgruppe auf den eingestellten Wert" diff --git a/src/assets/i18n/participant/en.json b/src/assets/i18n/participant/en.json index 22b6e9619505840804445769488b1167a52fba52..64aa3c5c572559ffedb35e65d4b35a50204fc9e8 100644 --- a/src/assets/i18n/participant/en.json +++ b/src/assets/i18n/participant/en.json @@ -111,7 +111,7 @@ "editing-done-hint": "Finish editing", "force-language-selection": "The language of the text input could not be detected automatically.", "add-manually": "Enter a keyword for your question. Separate several keywords with a comma.", - "select-keywords": "The text analysis suggests the following keywords. Which best characterise your question?" + "select-keywords": "The text analysis suggests keywords. Which best describe your question?" }, "comment-page": { "a11y-comment_input": "Enter your question", @@ -169,7 +169,7 @@ "show-more": "Show more", "show-less": "Show less", "delete": "Delete question", - "grammar-check": "Spell check", + "grammar-check": "Check text", "upvote": "upvotes", "downvote": "downvotes" }, @@ -179,6 +179,20 @@ "option-normal": "Your input:", "option-improved": "AI suggestion:" }, + "deepl-formality-select": { + "error": "The text could not be updated.", + "name": "Style", + "default": "Automatic", + "less": "Informal", + "more": "Formal" + }, + "explanation": { + "label": "Explanation", + "close": "Close", + "deepl": "## Text optimization\n\nTo make your question as readable and understandable as possible, we have it translated into German and back into English using the translation program [DeepL](https://www.deepl.com/translator).\n\nThe back translation is in almost all cases better than the original in terms of spelling, grammar, punctuation and language style.", + "spacy": "## Text analysis\n\nUsing NLP (Natural Language Processing) your question will be analyzed grammatically. The recognized nouns are put into their basic form, i.e. lemmatized, and suggested to you as keywords. For the text analysis we use the free NLP software [spaCy](https://spacy.io/). \n\nThe keywords can be used to filter the list of questions or to create a word cloud.", + "topic-cloud": "## Topic cloud\n\nThe topic cloud visualizes the frequency of the keywords: the larger the font, the more questions refer to the keyword. The rating of the questions is included in the font size.\n\nThe topic cloud serves as a navigator to all questions related to a selected keyword: If you click on a word in the cloud, you will get to the questions related to this keyword." + }, "home-page": { "exactly-8": "A key is a combination of 8 digits.", "no-room-found": "No session found with this key", @@ -203,14 +217,14 @@ "a11y-announcer": "You are now in the session with the key you entered.", "a11y-question_answer": "Opens the page where you can ask questions and vote up or down other questions.", "comments": "Questions", - "create-comment": "Ask a question!", + "create-comment": "Ask your questions!", "default-content-group": "Default", "description": "Description", "give-feedback": "Give feedback", "learn": "Learn", "live-announcer": "You're in the session now. To get information about key combinations press the Enter key or call the announcement later with the Escape key.", "live-feedback": "Live feedback", - "session-id": "Key", + "session-id": "Key code", "bonus-token": "Tokens for bonus stars", "bonus-token-header": "Tokens for bonus stars", "moderation-enabled": "The Session will be moderated.", @@ -265,6 +279,8 @@ "questions-blocked": "New questions blocked" }, "quill": { + "cancel": "Cancel", + "heading": "Input", "tooltip-remove": "Delete", "tooltip-action-save": "Save", "tooltip-action": "Edit",