From f34f69869f286b51798b4d9716b162ba9255fdbe Mon Sep 17 00:00:00 2001 From: Ruben Bimberg <ruben.bimberg@mni.thm.de> Date: Wed, 3 Nov 2021 22:50:38 +0100 Subject: [PATCH] Implement brainstorming --- .../create-comment.component.html | 1 + .../create-comment.component.ts | 8 +- .../topic-cloud-filter.component.html | 2 +- .../topic-cloud-filter.component.ts | 2 +- .../worker-dialog/worker-dialog-task.ts | 1 + .../comment-answer.component.html | 34 ++--- .../comment-answer.component.ts | 5 +- .../shared/header/header.component.html | 38 ------ .../shared/header/header.component.ts | 12 -- .../shared/tag-cloud/tag-cloud.component.html | 4 +- .../shared/tag-cloud/tag-cloud.component.ts | 126 +++++++++++++----- .../write-comment.component.scss | 4 + .../write-comment/write-comment.component.ts | 11 ++ src/app/models/comment.ts | 3 + src/app/services/http/comment.service.ts | 3 +- src/app/services/http/spacy.service.ts | 4 +- .../services/util/tag-cloud-data.service.ts | 25 +++- .../util/topic-cloud-admin.service.ts | 1 + src/app/utils/create-comment-keywords.ts | 29 ++-- src/app/utils/create-comment-wrapper.ts | 7 +- src/assets/i18n/creator/de.json | 12 +- src/assets/i18n/creator/en.json | 12 +- src/assets/i18n/home/de.json | 9 +- src/assets/i18n/home/en.json | 14 +- src/assets/i18n/participant/de.json | 12 +- src/assets/i18n/participant/en.json | 12 +- 26 files changed, 243 insertions(+), 148 deletions(-) diff --git a/src/app/components/shared/_dialogs/create-comment/create-comment.component.html b/src/app/components/shared/_dialogs/create-comment/create-comment.component.html index aaa4d15b1..5736f9de4 100644 --- a/src/app/components/shared/_dialogs/create-comment/create-comment.component.html +++ b/src/app/components/shared/_dialogs/create-comment/create-comment.component.html @@ -1,5 +1,6 @@ <ars-row ars-flex-box> <app-write-comment [confirmLabel]="'send'" + [brainstormingData]="brainstormingData" [isQuestionerNameEnabled]="true" [onSubmit]="this.forwardComment.bind(this)" [onDeeplSubmit]="this.closeDialog.bind(this)" 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 a76e736e5..29942d0f8 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 @@ -25,6 +25,7 @@ export class CreateCommentComponent implements OnInit { @Input() userRole: UserRole; @Input() roomId: string; @Input() tags: string[]; + @Input() brainstormingData: any; isSendingToSpacy = false; isModerator = false; @@ -65,13 +66,14 @@ export class CreateCommentComponent implements OnInit { comment.createdFromLecturer = this.userRole > 0; comment.tag = tag; comment.questionerName = name; + comment.brainstormingQuestion = !!this.brainstormingData; this.isSendingToSpacy = true; - this.openSpacyDialog(comment, text, forward); + this.openSpacyDialog(comment, text, forward, comment.brainstormingQuestion); } - openSpacyDialog(comment: Comment, rawText: string, forward: boolean): void { + openSpacyDialog(comment: Comment, rawText: string, forward: boolean, brainstorming: boolean): void { CreateCommentKeywords.generateKeywords(this.languagetoolService, this.deeplService, - this.spacyService, comment.body, forward, this.commentComponent.selectedLang) + this.spacyService, comment.body, brainstorming, forward, this.commentComponent.selectedLang) .subscribe(result => { this.isSendingToSpacy = false; comment.language = result.language; 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 fca3f52e7..29cbca706 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 @@ -101,7 +101,7 @@ </mat-error> </mat-form-field> <mat-form-field appearance="fill"> - <mat-label>Maximale Wortlänge</mat-label> + <mat-label>{{'content.brainstorming-word-length' | translate}}</mat-label> <input matInput autocomplete="off" type="number" 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 379fd5ebf..054b8de5b 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 @@ -156,7 +156,7 @@ export class TopicCloudFilterComponent implements OnInit, OnDestroy { getCommentCounts(comments: Comment[]): CommentsCount { const [data, users] = TagCloudDataService.buildDataFromComments(this._room.ownerId, this._currentModerators, - this._adminData, this.roomDataService, comments); + this._adminData, this.roomDataService, comments, false); const counts = new CommentsCount(); counts.comments = comments.length; counts.users = users.size; 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 35607461c..4dd4cd159 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 @@ -57,6 +57,7 @@ export class WorkerDialogTask { const currentComment = this._comments[currentIndex]; CreateCommentKeywords.generateKeywords(this.languagetoolService, this.deeplService, this.spacyService, currentComment.body, + currentComment.brainstormingQuestion, !currentComment.keywordsFromQuestioner || currentComment.keywordsFromQuestioner.length === 0, currentComment.language.toLowerCase() as Lang) .subscribe((result) => this.finishSpacyCall(currentIndex, result, currentComment.language)); diff --git a/src/app/components/shared/comment-answer/comment-answer.component.html b/src/app/components/shared/comment-answer/comment-answer.component.html index 53664c508..3a9d7a7a7 100644 --- a/src/app/components/shared/comment-answer/comment-answer.component.html +++ b/src/app/components/shared/comment-answer/comment-answer.component.html @@ -1,10 +1,10 @@ <div fxLayout="column" fxLayoutAlign="center" *ngIf="!isLoading" - #container (document:keyup)="checkForEscape($event)" - (document:click)="checkForBackDropClick($event, container)"> + (document:click)="checkForBackDropClick($event, container.firstChild, writeCommentCard)"> <div fxLayout="row" + #container fxLayoutAlign="center"> <app-comment [comment]="comment" [user]="user" @@ -12,20 +12,22 @@ </div> <div fxLayout="row" fxLayoutAlign="center"> - <mat-card class="answer border-answer" - *ngIf="!isStudent || answer"> - <app-write-comment [isModerator]="userRole > 0" - [isCommentAnswer]="true" - [placeholder]="'comment-page.your-answer'" - [onClose]="openDeleteAnswerDialog()" - [onSubmit]="saveAnswer()" - [disableCancelButton]="!answer && commentComponent && commentComponent.commentData.currentText.length > 0" - [confirmLabel]="'save-answer'" - [cancelLabel]="'delete-answer'" - [additionalTemplate]="editAnswer" - [enabled]="!isStudent && (edit || !answer)"> - </app-write-comment> - </mat-card> + <div #writeCommentCard fxLayout="row"> + <mat-card class="answer border-answer" + *ngIf="!isStudent || answer"> + <app-write-comment [isModerator]="userRole > 0" + [isCommentAnswer]="true" + [placeholder]="'comment-page.your-answer'" + [onClose]="openDeleteAnswerDialog()" + [onSubmit]="saveAnswer()" + [disableCancelButton]="!answer && commentComponent && commentComponent.commentData.currentText.length > 0" + [confirmLabel]="'save-answer'" + [cancelLabel]="'delete-answer'" + [additionalTemplate]="editAnswer" + [enabled]="!isStudent && (edit || !answer)"> + </app-write-comment> + </mat-card> + </div> </div> </div> diff --git a/src/app/components/shared/comment-answer/comment-answer.component.ts b/src/app/components/shared/comment-answer/comment-answer.component.ts index 7b9cc32a5..fd61ef52a 100644 --- a/src/app/components/shared/comment-answer/comment-answer.component.ts +++ b/src/app/components/shared/comment-answer/comment-answer.component.ts @@ -87,8 +87,9 @@ export class CommentAnswerComponent implements OnInit, OnDestroy { } } - checkForBackDropClick(event: PointerEvent, element: HTMLElement) { - if (event.target && !element.contains(event.target as Node)) { + checkForBackDropClick(event: PointerEvent, ...elements: Node[]) { + const target = event.target as Node; + if (event.target && !elements.some(e => e.contains(target))) { this.goBackToCommentList(); } } diff --git a/src/app/components/shared/header/header.component.html b/src/app/components/shared/header/header.component.html index a6e293c95..1cc20c002 100644 --- a/src/app/components/shared/header/header.component.html +++ b/src/app/components/shared/header/header.component.html @@ -245,44 +245,6 @@ </ng-container> - <button mat-menu-item - tabindex="0" - *ngIf="router.url.endsWith('/tagcloud')" - (click)="navigateQuestionBoard()"> - <mat-icon> - forum - </mat-icon> - <span>{{'header.back-to-questionboard' | translate}}</span> - </button> - - <button mat-menu-item - *ngIf="userRole > 0 && router.url.endsWith('/tagcloud')" - tabindex="0" - (click)="navigateTopicCloudConfig()"> - <mat-icon aria-label="Configuration Icon">cloud</mat-icon> - <span>{{'header.tag-cloud-config' | translate}}</span> - </button> - - <button mat-menu-item - *ngIf="userRole > 0 && router.url.endsWith('/tagcloud')" - tabindex="0" - (click)="navigateTopicCloudAdministration()"> - <mat-icon aria-hidden="false" - aria-label="Control Icon">cloud - </mat-icon> - <span>{{'header.tag-cloud-administration' | translate}}</span> - </button> - - - <button mat-menu-item - tabindex="0" - *ngIf="userRole > 0 && router.url.endsWith('/tagcloud')" - (click)="startWorkerDialog()"> - <mat-icon>cloud - </mat-icon> - <span>{{'header.update-spacy-keywords' | translate}}</span> - </button> - <button mat-menu-item *ngIf="router.url.endsWith('/comments') && !router.url.includes('/comment/') && !router.url.endsWith('tagcloud')" (click)="navigateExportQuestions()" diff --git a/src/app/components/shared/header/header.component.ts b/src/app/components/shared/header/header.component.ts index 91bcbd193..173c6a0cb 100644 --- a/src/app/components/shared/header/header.component.ts +++ b/src/app/components/shared/header/header.component.ts @@ -384,21 +384,9 @@ export class HeaderComponent implements OnInit, AfterViewInit { confirmDialogRef.componentInstance.userRole = this.userRole; } - public navigateTopicCloudConfig() { - this.eventService.broadcast('navigate', 'topicCloudConfig'); - } - - public navigateTopicCloudAdministration() { - this.eventService.broadcast('navigate', 'topicCloudAdministration'); - } - public blockQuestions() { // flip state if clicked this.room.questionsBlocked = !this.room.questionsBlocked; this.roomService.updateRoom(this.room).subscribe(); } - - public startWorkerDialog() { - WorkerConfigDialogComponent.addTask(this.dialog, this.room); - } } diff --git a/src/app/components/shared/tag-cloud/tag-cloud.component.html b/src/app/components/shared/tag-cloud/tag-cloud.component.html index 0e15bcb80..961d90540 100644 --- a/src/app/components/shared/tag-cloud/tag-cloud.component.html +++ b/src/app/components/shared/tag-cloud/tag-cloud.component.html @@ -8,7 +8,7 @@ <app-cloud-configuration #cloudComponent [parent]="this"></app-cloud-configuration> </mat-drawer> <mat-drawer-content> - <h1 *ngIf="user && user.role > 0 && question" class="tag-cloud-brainstorming-question mat-display-1"> + <h1 *ngIf="question" class="tag-cloud-brainstorming-question mat-display-1"> {{question}} </h1> <ars-fill ars-flex-box> @@ -35,7 +35,7 @@ mat-icon-button aria-labelledby="add" class="fab_add_comment" - (click)="createCommentWrapper.openCreateDialog(user, userRole).subscribe()" + (click)="writeComment()" matTooltip="{{ 'comment-list.add-comment' | translate }}"> <mat-icon>add</mat-icon> </button> diff --git a/src/app/components/shared/tag-cloud/tag-cloud.component.ts b/src/app/components/shared/tag-cloud/tag-cloud.component.ts index 09bec4288..0bf67cde2 100644 --- a/src/app/components/shared/tag-cloud/tag-cloud.component.ts +++ b/src/app/components/shared/tag-cloud/tag-cloud.component.ts @@ -1,4 +1,4 @@ -import { AfterContentInit, Component, OnDestroy, OnInit, ViewChild } from '@angular/core'; +import { AfterContentInit, Component, ComponentRef, EventEmitter, OnDestroy, OnInit, ViewChild } from '@angular/core'; import { CloudData, @@ -29,12 +29,16 @@ import { TagCloudDataService, TagCloudDataTagEntry } from '../../../services/uti import { WsRoomService } from '../../../services/websockets/ws-room.service'; import { CloudParameters, CloudTextStyle } from '../../../utils/cloud-parameters'; import { SmartDebounce } from '../../../utils/smart-debounce'; -import { Theme } from '../../../../theme/Theme'; +import { Palette, Theme } from '../../../../theme/Theme'; import { MatDrawer } from '@angular/material/sidenav'; import { DeviceInfoService } from '../../../services/util/device-info.service'; import { SyncFence } from '../../../utils/SyncFence'; import { Subscription } from 'rxjs'; import { CommentListFilter } from '../comment-list/comment-list.filter'; +import { ArsComposeService } from '../../../../../projects/ars/src/lib/services/ars-compose.service'; +import { HeaderService } from '../../../services/util/header.service'; +import { WorkerConfigDialogComponent } from '../_dialogs/worker-config-dialog/worker-config-dialog.component'; +import { KeywordOrFulltext } from '../_dialogs/topic-cloud-administration/TopicCloudAdminData'; class CustomPosition implements Position { left: number; @@ -85,6 +89,7 @@ export class TagCloudComponent implements OnInit, OnDestroy, AfterContentInit { @ViewChild(TagCloudPopUpComponent) popup: TagCloudPopUpComponent; @ViewChild(MatDrawer) drawer: MatDrawer; + onDestroyListener: EventEmitter<void> = new EventEmitter<void>(); roomId: string; user: User; room: Room; @@ -105,7 +110,6 @@ export class TagCloudComponent implements OnInit, OnDestroy, AfterContentInit { userRole: UserRole; data: TagComment[] = []; isLoading = true; - headerInterface = null; themeSubscription = null; createCommentWrapper: CreateCommentWrapper = null; question = ''; @@ -122,6 +126,7 @@ export class TagCloudComponent implements OnInit, OnDestroy, AfterContentInit { private _currentTheme: Theme; private _syncFenceBuildCloud: SyncFence; private _eventFilterSubscription: Subscription; + private _pushCurrentBrainstorming = false; constructor(private commentService: CommentService, private langService: LanguageService, @@ -130,6 +135,8 @@ export class TagCloudComponent implements OnInit, OnDestroy, AfterContentInit { private notificationService: NotificationService, public eventService: EventService, private authenticationService: AuthenticationService, + private composeService: ArsComposeService, + private headerService: HeaderService, private route: ActivatedRoute, protected roomService: RoomService, private themeService: ThemeService, @@ -143,13 +150,14 @@ export class TagCloudComponent implements OnInit, OnDestroy, AfterContentInit { this.langService.langEmitter.subscribe(lang => { this.translateService.use(lang); }); + this.userRole = this.route.snapshot.data.roles[0]; this._currentSettings = TagCloudComponent.getCurrentCloudParameters(); this._calcCanvas = document.createElement('canvas'); this._calcRenderContext = this._calcCanvas.getContext('2d'); this._syncFenceBuildCloud = new SyncFence(2, - () => this.dataManager.bindToRoom(this.room, this.userRole, this.user.id)); + () => this.dataManager.bindToRoom(this.room, this.userRole, this.user.id, this.brainstormingActive)); this._eventFilterSubscription = eventService.on('tagCloudPassFilterData').subscribe((data: any) => { - if (data.brainstorming) { + if (data.brainstorming.brainstormingActive) { this.brainstormingActive = true; this.question = data.brainstorming.question as string; this.maxWordCount = data.brainstorming.maxWordCount as number; @@ -157,6 +165,9 @@ export class TagCloudComponent implements OnInit, OnDestroy, AfterContentInit { } else { this.brainstormingActive = false; } + if (this.userRole > 0) { + this._pushCurrentBrainstorming = true; + } localStorage.setItem('brainstormingActive', this.brainstormingActive ? 'true' : 'false'); (data.filter as CommentListFilter).save('cloudFilter'); this._eventFilterSubscription.unsubscribe(); @@ -180,29 +191,7 @@ export class TagCloudComponent implements OnInit, OnDestroy, AfterContentInit { this._eventFilterSubscription.unsubscribe(); this.brainstormingActive = localStorage.getItem('brainstormingActive') === 'true'; } - this.userRole = this.route.snapshot.data.roles[0]; this.updateGlobalStyles(); - this.headerInterface = this.eventService.on<string>('navigate').subscribe(e => { - if (e === 'createQuestion') { - this.createCommentWrapper.openCreateDialog(this.user, this.userRole).subscribe(); - } else if (e === 'topicCloudConfig') { - if (this.drawer.opened) { - this.drawer.close(); - } else { - this.drawer.open(); - } - } else if (e === 'topicCloudAdministration') { - this.dialog.open(TopicCloudAdministrationComponent, { - minWidth: '50%', - maxHeight: '95%', - data: { - userRole: this.userRole - } - }); - } else if (e === 'questionBoard') { - this.router.navigate(['../'], { relativeTo: this.route }); - } - }); this.dataManager.getData().subscribe(data => { if (!data) { return; @@ -226,7 +215,6 @@ export class TagCloudComponent implements OnInit, OnDestroy, AfterContentInit { this.authenticationService.guestLogin(UserRole.PARTICIPANT).subscribe(r => { this.roomService.getRoomByShortId(this.shortId).subscribe(room => { this.room = room; - this.retrieveTagCloudSettings(room); this.roomId = room.id; this._subscriptionRoom = this.wsRoomService.getRoomStream(this.roomId).subscribe(msg => { const message = JSON.parse(msg.body); @@ -236,6 +224,20 @@ export class TagCloudComponent implements OnInit, OnDestroy, AfterContentInit { this.retrieveTagCloudSettings(message.payload.changes); } }); + if (this._pushCurrentBrainstorming) { + const data = JSON.parse(room.tagCloudSettings) || {}; + if (data.admin?.keywordORfulltext === KeywordOrFulltext.keyword) { + data.admin.keywordORfulltext = KeywordOrFulltext.both; + } + data.brainstorming = this.brainstormingActive ? { + question: this.question, + maxWordLength: this.maxWordLength, + maxWordCount: this.maxWordCount + } : undefined; + room.tagCloudSettings = JSON.stringify(data); + this.roomService.updateRoom(room).subscribe(); + } + this.retrieveTagCloudSettings(room); this.directSend = this.room.directSend; this.createCommentWrapper = new CreateCommentWrapper(this.translateService, this.notificationService, this.commentService, this.dialog, this.room); @@ -259,6 +261,7 @@ export class TagCloudComponent implements OnInit, OnDestroy, AfterContentInit { } ngAfterContentInit() { + this.initNavigation(); this._calcFont = window.getComputedStyle(document.getElementById('tagCloudComponent')).fontFamily; setTimeout(() => this._syncFenceBuildCloud.resolveCondition(CONDITION_BUILT)); this.dataManager.updateDemoData(this.translateService); @@ -270,12 +273,12 @@ export class TagCloudComponent implements OnInit, OnDestroy, AfterContentInit { if (customTagCloudStyles) { customTagCloudStyles.sheet.disabled = true; } - this.headerInterface.unsubscribe(); this.themeSubscription.unsubscribe(); this.dataManager.unbindRoom(); if (this._subscriptionRoom) { this._subscriptionRoom.unsubscribe(); } + this.onDestroyListener.emit(); } get tagCloudDataManager(): TagCloudDataService { @@ -317,6 +320,14 @@ export class TagCloudComponent implements OnInit, OnDestroy, AfterContentInit { this.updateTagCloud(); } + writeComment() { + this.createCommentWrapper.openCreateDialog(this.user, this.userRole, this.brainstormingActive ? { + question: this.question, + maxWordLength: this.maxWordLength, + maxWordCount: this.maxWordCount + } : undefined).subscribe(); + } + rebuildData() { if (!this.child || !this.dataManager.currentData) { return; @@ -339,7 +350,10 @@ export class TagCloudComponent implements OnInit, OnDestroy, AfterContentInit { if (rotation === null || this._currentSettings.randomAngles) { rotation = Math.floor(Math.random() * 30 - 15); } - const filteredTag = maskKeyword(tag); + let filteredTag = maskKeyword(tag); + if (this.brainstormingActive && filteredTag.length > this.maxWordLength) { + filteredTag = filteredTag.substr(0, this.maxWordLength - 1) + '…'; + } newElements.push(new TagComment(filteredTag, tag, rotation, tagData.weight, tagData, newElements.length)); } } @@ -415,9 +429,11 @@ export class TagCloudComponent implements OnInit, OnDestroy, AfterContentInit { admin.endDate = data.admin.endDate; admin.scorings = data.admin.scorings; data.admin = undefined; - this.question = data.brainstorming?.question; - this.maxWordLength = data.brainstorming?.maxWordLength; - this.maxWordCount = data.brainstorming?.maxWordCount; + if (this.brainstormingActive) { + this.question = data.brainstorming?.question; + this.maxWordLength = data.brainstorming?.maxWordLength; + this.maxWordCount = data.brainstorming?.maxWordCount; + } data.brainstorming = undefined; this.topicCloudAdmin.setAdminData(admin, false, this.userRole); if (this.deviceInfo.isCurrentlyMobile) { @@ -447,6 +463,50 @@ export class TagCloudComponent implements OnInit, OnDestroy, AfterContentInit { }); } + private initNavigation() { + /* eslint-disable @typescript-eslint/no-shadow */ + const list: ComponentRef<any>[] = this.composeService.builder(this.headerService.getHost(), e => { + e.menuItem({ + translate: this.headerService.getTranslate(), + icon: 'forum', + text: 'header.back-to-questionboard', + callback: () => this.router.navigate(['../'], { relativeTo: this.route }), + condition: () => true + }); + e.menuItem({ + translate: this.headerService.getTranslate(), + icon: 'cloud', + text: 'header.tag-cloud-config', + callback: () => this.drawer.toggle(), + condition: () => this.userRole > UserRole.PARTICIPANT + }); + e.menuItem({ + translate: this.headerService.getTranslate(), + icon: 'cloud', + text: 'header.tag-cloud-administration', + callback: () => this.dialog.open(TopicCloudAdministrationComponent, { + minWidth: '50%', + maxHeight: '95%', + data: { + userRole: this.userRole + } + }), + condition: () => this.userRole > UserRole.PARTICIPANT && !this.brainstormingActive + }); + e.menuItem({ + translate: this.headerService.getTranslate(), + icon: 'cloud', + text: 'header.update-spacy-keywords', + callback: () => WorkerConfigDialogComponent.addTask(this.dialog, this.room), + condition: () => this.userRole > UserRole.PARTICIPANT && !this.brainstormingActive + }); + }); + this.onDestroyListener.subscribe(() => { + list.forEach(e => e.destroy()); + }); + /* eslint-enable @typescript-eslint/no-shadow */ + } + private redraw(dataUpdate: boolean): void { if (this.child === undefined) { return; 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 b6430bf80..7e8e63f7c 100644 --- a/src/app/components/shared/write-comment/write-comment.component.scss +++ b/src/app/components/shared/write-comment/write-comment.component.scss @@ -86,6 +86,10 @@ mat-hint { font-style: italic; } +::ng-deep .placeholder .mat-radio-label-content { + width: 100%; +} + .lang-confidence { color: var(--on-cancel); background-color: var(--cancel); 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 4008918a5..c0a1b86e7 100644 --- a/src/app/components/shared/write-comment/write-comment.component.ts +++ b/src/app/components/shared/write-comment/write-comment.component.ts @@ -38,6 +38,7 @@ export class WriteCommentComponent implements OnInit { @Input() placeholder = 'comment-page.enter-comment'; @Input() i18nSection = 'comment-page'; @Input() isQuestionerNameEnabled = false; + @Input() brainstormingData: any; comment: Comment; selectedTag: string; maxTextCharacters = 500; @@ -66,6 +67,10 @@ export class WriteCommentComponent implements OnInit { ngOnInit(): void { this.translateService.use(localStorage.getItem('currentLang')); + if (this.brainstormingData) { + this.translateService.get('comment-page.brainstorming-placeholder', this.brainstormingData) + .subscribe(msg => this.placeholder = msg); + } if (this.isCommentAnswer) { this.maxTextCharacters = this.isModerator ? 2000 : 0; } else { @@ -92,6 +97,12 @@ export class WriteCommentComponent implements OnInit { allowed = !this.questionerNameFormControl.hasError('minlength') && !this.questionerNameFormControl.hasError('maxlength'); } + if (this.brainstormingData && this.commentData.currentText.split(/\s+/g).length - 1 > + this.brainstormingData.maxWordCount) { + this.translateService.get('comment-page.error-comment-brainstorming', this.brainstormingData) + .subscribe(msg => this.notification.show(msg)); + allowed = false; + } if (ViewCommentDataComponent.checkInputData(this.commentData.currentData, this.commentData.currentText, this.translateService, this.notification, this.maxTextCharacters, this.maxDataCharacters) && allowed) { func(this.commentData.currentData, this.commentData.currentText, this.selectedTag, diff --git a/src/app/models/comment.ts b/src/app/models/comment.ts index 26c3ba2d7..453a4b131 100644 --- a/src/app/models/comment.ts +++ b/src/app/models/comment.ts @@ -28,6 +28,7 @@ export class Comment { language: Language; questionerName: string; createdBy; + brainstormingQuestion: boolean; constructor(roomId: string = '', creatorId: string = '', @@ -50,6 +51,7 @@ export class Comment { downvotes = 0, language = Language.auto, questionerName: string = null, + brainstormingQuestion = false, createdBy?: any) { this.id = ''; this.roomId = roomId; @@ -75,6 +77,7 @@ export class Comment { this.language = language; this.createdBy = createdBy; this.questionerName = questionerName; + this.brainstormingQuestion = brainstormingQuestion; } static mapModelToLanguage(model: Model): Language { diff --git a/src/app/services/http/comment.service.ts b/src/app/services/http/comment.service.ts index 8841ca6bf..369c41a8b 100644 --- a/src/app/services/http/comment.service.ts +++ b/src/app/services/http/comment.service.ts @@ -86,7 +86,8 @@ export class CommentService extends BaseHttpService { read: comment.read, creationTimestamp: comment.timestamp, tag: comment.tag, keywordsFromSpacy: JSON.stringify(comment.keywordsFromSpacy), keywordsFromQuestioner: JSON.stringify(comment.keywordsFromQuestioner), - language: comment.language, questionerName: comment.questionerName + language: comment.language, questionerName: comment.questionerName, + brainstormingQuestion: comment.brainstormingQuestion }, httpOptions).pipe( tap(_ => ''), catchError(this.handleError<Comment>('addComment')) diff --git a/src/app/services/http/spacy.service.ts b/src/app/services/http/spacy.service.ts index dca759532..a18a69fa9 100644 --- a/src/app/services/http/spacy.service.ts +++ b/src/app/services/http/spacy.service.ts @@ -29,10 +29,10 @@ export class SpacyService extends BaseHttpService { return DEFAULT_NOUN_LABELS[model]; } - getKeywords(text: string, model: Model): Observable<SpacyKeyword[]> { + getKeywords(text: string, model: Model, brainstorming: boolean): Observable<SpacyKeyword[]> { const url = '/spacy'; return this.checkCanSendRequest('getKeywords') || this.http - .post<SpacyKeyword[]>(url, { text, model }, httpOptions) + .post<SpacyKeyword[]>(url, { text, model, brainstorming: String(!!brainstorming) }, httpOptions) .pipe( tap(_ => ''), timeout(2500), diff --git a/src/app/services/util/tag-cloud-data.service.ts b/src/app/services/util/tag-cloud-data.service.ts index 1078ba1b5..ad25f358d 100644 --- a/src/app/services/util/tag-cloud-data.service.ts +++ b/src/app/services/util/tag-cloud-data.service.ts @@ -86,6 +86,7 @@ export class TagCloudDataService { private _currentModerators: string[]; private _currentOwner: string; private readonly _smartDebounce = new SmartDebounce(200, 3_000); + private _isBrainstorming: boolean; constructor(private _tagCloudAdmin: TopicCloudAdminService, private _roomDataService: RoomDataService, @@ -108,10 +109,14 @@ export class TagCloudDataService { moderators: string[], adminData: TopicCloudAdminData, roomDataService: RoomDataService, - comments: Comment[]): [TagCloudData, Set<number>] { + comments: Comment[], + brainstorming: boolean): [TagCloudData, Set<number>] { const data: TagCloudData = new Map<string, TagCloudDataTagEntry>(); const users = new Set<number>(); for (const comment of comments) { + if (brainstorming !== comment.brainstormingQuestion) { + continue; + } TopicCloudAdminService.approveKeywordsOfComment(comment, roomDataService, adminData, (keyword: SpacyKeyword, isFromQuestioner: boolean) => { let current: TagCloudDataTagEntry = data.get(keyword.text); @@ -166,18 +171,20 @@ export class TagCloudDataService { users.add(comment.userNumber); } return [ - new Map<string, TagCloudDataTagEntry>([...data].filter(v => TopicCloudAdminService.isTopicAllowed(adminData, - v[1].comments.length, v[1].distinctUsers.size, v[1].cachedUpVotes, v[1].firstTimeStamp, v[1].lastTimeStamp))), + new Map<string, TagCloudDataTagEntry>([...data].filter(v => brainstorming || + TopicCloudAdminService.isTopicAllowed(adminData, v[1].comments.length, v[1].distinctUsers.size, + v[1].cachedUpVotes, v[1].firstTimeStamp, v[1].lastTimeStamp))), users ]; } - bindToRoom(room: Room, userRole: UserRole, userId: string): void { + bindToRoom(room: Room, userRole: UserRole, userId: string, isBrainstorming: boolean): void { if (this._subscriptionAdminData) { throw new Error('Room already bound.'); } this._currentModerators = null; - this._currentFilter = CommentListFilter.loadFilter(); + this._isBrainstorming = isBrainstorming; + this._currentFilter = CommentListFilter.loadFilter('cloudFilter'); this._currentFilter.updateRoom(room); this._roomId = room.id; this._currentOwner = room.ownerId; @@ -347,6 +354,12 @@ export class TagCloudDataService { private calculateWeight(tagData: TagCloudDataTagEntry): number { const scorings = this._adminData.scorings; + if (this._isBrainstorming) { + return tagData.comments.length * scorings.countComments.score + + tagData.distinctUsers.size * scorings.countUsers.score + + tagData.commentsByModerators * scorings.countKeywordByModerator.score + + tagData.commentsByCreator * scorings.countKeywordByCreator.score; + } return tagData.comments.length * scorings.countComments.score + tagData.distinctUsers.size * scorings.countUsers.score + tagData.generatedByQuestionerCount * scorings.countSelectedByQuestioner.score + @@ -367,7 +380,7 @@ export class TagCloudDataService { const filteredComments = this._currentFilter.checkAll(this._lastFetchedComments); currentMeta.commentCount = filteredComments.length; const [data, users] = TagCloudDataService.buildDataFromComments(this._currentOwner, this._currentModerators, - this._adminData, this._roomDataService, filteredComments); + this._adminData, this._roomDataService, filteredComments, this._isBrainstorming); let minWeight = null; let maxWeight = null; for (const value of data.values()) { diff --git a/src/app/services/util/topic-cloud-admin.service.ts b/src/app/services/util/topic-cloud-admin.service.ts index 42836978f..9471c7d52 100644 --- a/src/app/services/util/topic-cloud-admin.service.ts +++ b/src/app/services/util/topic-cloud-admin.service.ts @@ -62,6 +62,7 @@ export class TopicCloudAdminService { endDate: admin.endDate, scorings: admin.scorings }; + settings.brainstorming = JSON.parse(room.tagCloudSettings)?.brainstorming; room.tagCloudSettings = JSON.stringify(settings); } diff --git a/src/app/utils/create-comment-keywords.ts b/src/app/utils/create-comment-keywords.ts index 26cc24f06..17a84259b 100644 --- a/src/app/utils/create-comment-keywords.ts +++ b/src/app/utils/create-comment-keywords.ts @@ -65,12 +65,13 @@ export class CreateCommentKeywords { deeplService: DeepLService, spacyService: SpacyService, body: string, + brainstorming: boolean, useDeepl: boolean = false, language: Language = 'auto'): Observable<KeywordsResult> { const text = ViewCommentDataComponent.getTextFromData(body); return languagetoolService.checkSpellings(text, language).pipe( switchMap(result => this.spacyKeywordsFromLanguagetoolResult(languagetoolService, deeplService, - spacyService, text, body, language, result, useDeepl)), + spacyService, text, body, language, result, useDeepl, brainstorming)), catchError((err) => of({ keywords: [], language: CommentLanguage.auto, @@ -87,13 +88,14 @@ export class CreateCommentKeywords { body: string, selectedLanguage: Language, result: LanguagetoolResult, - useDeepl: boolean): Observable<KeywordsResult> { + useDeepl: boolean, + brainstorming: boolean): Observable<KeywordsResult> { const wordCount = text.trim().split(' ').length; const hasConfidence = selectedLanguage === 'auto' ? result.language.detectedLanguage.confidence >= 0.5 : true; const errorQuotient = (result.matches.length * 100) / wordCount; - if (!hasConfidence || + if (!brainstorming && (!hasConfidence || errorQuotient > ERROR_QUOTIENT_USE_DEEPL || - (!useDeepl && errorQuotient > ERROR_QUOTIENT_WELL_SPELLED)) { + (!useDeepl && errorQuotient > ERROR_QUOTIENT_WELL_SPELLED))) { return of({ keywords: [], language: CommentLanguage.auto, @@ -102,7 +104,7 @@ export class CreateCommentKeywords { } const escapedText = this.escapeForSpacy(text); let textLangObservable = of(escapedText); - if (useDeepl && errorQuotient > ERROR_QUOTIENT_WELL_SPELLED) { + if (!brainstorming && useDeepl && errorQuotient > ERROR_QUOTIENT_WELL_SPELLED) { let target = TargetLang.EN_US; const code = result.language.detectedLanguage.code.toUpperCase().split('-')[0]; if (code.startsWith(SourceLang.EN)) { @@ -116,7 +118,7 @@ export class CreateCommentKeywords { return textLangObservable.pipe( switchMap((textForSpacy) => this.callSpacy(spacyService, textForSpacy, languagetoolService.isSupportedLanguage(result.language.code as Language), selectedLanguage, - languagetoolService.mapLanguageToSpacyModel(result.language.code as Language))) + languagetoolService.mapLanguageToSpacyModel(result.language.code as Language), brainstorming)) ); } @@ -124,7 +126,8 @@ export class CreateCommentKeywords { text: string, isResultLangSupported: boolean, selectedLanguage: Language, - commentModel: Model): Observable<KeywordsResult> { + commentModel: Model, + brainstorming: boolean): Observable<KeywordsResult> { const selectedLangExtend = selectedLanguage[2] === '-' ? selectedLanguage.substr(0, 2) : selectedLanguage; let finalLanguage: CommentLanguage; @@ -134,13 +137,23 @@ export class CreateCommentKeywords { finalLanguage = CommentLanguage[selectedLangExtend]; } if (!isResultLangSupported || !CURRENT_SUPPORTED_LANGUAGES.includes(commentModel)) { + if (brainstorming) { + return of({ + keywords: text.split(/\s+/g).filter(e => e.length).map(newText => ({ + dep: ['ROOT'], + text: newText + })), + language: finalLanguage, + resultType: KeywordsResultType.successful + }); + } return of({ keywords: [], language: finalLanguage, resultType: KeywordsResultType.languageNotSupported } as KeywordsResult); } - return spacyService.getKeywords(text, commentModel).pipe( + return spacyService.getKeywords(text, commentModel, brainstorming).pipe( map(keywords => ({ keywords, language: finalLanguage, diff --git a/src/app/utils/create-comment-wrapper.ts b/src/app/utils/create-comment-wrapper.ts index 576e08ec3..d53d59c27 100644 --- a/src/app/utils/create-comment-wrapper.ts +++ b/src/app/utils/create-comment-wrapper.ts @@ -7,7 +7,7 @@ import { Room } from '../models/room'; import { Comment } from '../models/comment'; import { NotificationService } from '../services/util/notification.service'; import { CommentService } from '../services/http/comment.service'; -import { observable, Observable, of } from 'rxjs'; +import { Observable, of } from 'rxjs'; import { flatMap } from 'rxjs/internal/operators'; import { tap } from 'rxjs/operators'; import { MatSnackBarConfig } from '@angular/material/snack-bar'; @@ -21,7 +21,7 @@ export class CreateCommentWrapper { private room: Room) { } - openCreateDialog(user: User, userRole: UserRole): Observable<Comment> { + openCreateDialog(user: User, userRole: UserRole, brainstormingData: any = undefined): Observable<Comment> { const dialogRef = this.dialog.open(CreateCommentComponent, { width: '900px', maxWidth: '100%', @@ -31,7 +31,8 @@ export class CreateCommentWrapper { dialogRef.componentInstance.user = user; dialogRef.componentInstance.userRole = userRole; dialogRef.componentInstance.roomId = this.room.id; - dialogRef.componentInstance.tags = this.room.tags || []; + dialogRef.componentInstance.tags = (!brainstormingData && this.room.tags) || []; + dialogRef.componentInstance.brainstormingData = brainstormingData; return dialogRef.afterClosed().pipe( flatMap((comment: Comment) => comment ? this.send(comment) : of<Comment>(null)) ); diff --git a/src/assets/i18n/creator/de.json b/src/assets/i18n/creator/de.json index 87fb3ceed..5ab98733f 100644 --- a/src/assets/i18n/creator/de.json +++ b/src/assets/i18n/creator/de.json @@ -224,7 +224,9 @@ "show-comment-without-filter": "Vulgäre Wörter anzeigen", "upvote": "positiv:", "downvote": "negativ:", - "questioner-name": "Selbst vergebener Name" + "questioner-name": "Selbst vergebener Name", + "brainstorming-placeholder": "Schreibe deine Wörter für das Brainstorming. Maximal {{maxWordCount}} Wörter mit jeweils maximal {{maxWordLength}} Zeichen.", + "error-comment-brainstorming": "Die Brainstorming Session erlaubt nur {{maxWordCount}} Wörter" }, "content": { "abort": "Abbrechen", @@ -625,5 +627,13 @@ "questioner-name": "Dein Name, wenn du willst …", "a11y-questioner-name": "Hier kannst du, wenn du willst, deinen Namen eingeben. Dieser ist für alle an diesem Kommentar sichtbar.", "name-length-error": "Der Name muss zwischen 2 und 20 Zeichen lang sein." + }, + "worker-config": { + "heading": "Stichwörter extrahieren", + "label": "Wähle, aus welchen Fragen mittels Künstlicher Intelligenz (siehe »Einführung« in der Fußzeile) Stichwörter extrahiert werden sollen:", + "normal": "Alle Fragen des Raumes neu analysieren", + "only-failed": "Nur Fragen, die nicht analysiert worden sind", + "continue": "Weiter", + "cancel": "Abbrechen" } } diff --git a/src/assets/i18n/creator/en.json b/src/assets/i18n/creator/en.json index c9f7ae322..60c28801b 100644 --- a/src/assets/i18n/creator/en.json +++ b/src/assets/i18n/creator/en.json @@ -225,7 +225,9 @@ "show-comment-without-filter": "Show vulgar words", "upvote": "upvotes:", "downvote": "downvotes:", - "questioner-name": "Self assigned name" + "questioner-name": "Self assigned name", + "brainstorming-placeholder": "Write your words for the brainstorm. Maximum {{maxWordCount}} words, each with a maximum of {{maxWordLength}} characters.", + "error-comment-brainstorming": "The brainstorming session allows only {{maxWordCount}} words" }, "content": { "abort": "Abort", @@ -623,5 +625,13 @@ "questioner-name": "Your name, if you want …", "a11y-questioner-name": "Here you can enter your name if you want. This is visible for everyone at this comment.", "name-length-error": "The name must be between 2 and 20 characters." + }, + "worker-config": { + "heading": "Extract keywords", + "label": "Choose from which questions to extract keywords using Artificial Intelligence (see »Introduction« in the footer):", + "normal": "Reanalyze all questions in the room", + "only-failed": "Only questions that have not been analyzed", + "continue": "Continue", + "cancel": "Cancel" } } diff --git a/src/assets/i18n/home/de.json b/src/assets/i18n/home/de.json index f75e3b04e..2236ca034 100644 --- a/src/assets/i18n/home/de.json +++ b/src/assets/i18n/home/de.json @@ -51,6 +51,7 @@ "reset": "Zurücksetzen", "brainstorming-question": "Überschrift des Brainstorming-Boards", "brainstorming-word-count": "Maximale Anzahl Wörter", + "brainstorming-word-length": "Maximale Wortlänge", "field-required": "Dieses Feld wird benötigt", "field-hint-number": "Eingabe zwischen {{min}} und {{max}}", "field-too-low": "Deine Eingabe ist zu niedrig. Sie muss mindestens {{min}} sein.", @@ -401,14 +402,6 @@ "add-successful": "Wort hinzugefügt", "remove-successful": "Wort entfernt" }, - "worker-config": { - "heading": "Stichwörter extrahieren", - "label": "Wähle, aus welchen Fragen mittels Künstlicher Intelligenz (siehe »Einführung« in der Fußzeile) Stichwörter extrahiert werden sollen:", - "normal": "Alle Fragen des Raumes neu analysieren", - "only-failed": "Nur Fragen, die nicht analysiert worden sind", - "continue": "Weiter", - "cancel": "Abbrechen" - }, "worker-dialog": { "running": "Laufend", "room-name": "Raum", diff --git a/src/assets/i18n/home/en.json b/src/assets/i18n/home/en.json index b8e7cabf8..4054fefc8 100644 --- a/src/assets/i18n/home/en.json +++ b/src/assets/i18n/home/en.json @@ -158,6 +158,12 @@ "continue": "Continue", "reset": "Reset", "brainstorming-question": "Heading of the brainstorming board", + "brainstorming-word-count": "Maximum number of words", + "brainstorming-word-length": "Maximum word length", + "field-required": "This field is required", + "field-hint-number": "Input between {{min}} and {{max}}", + "field-too-low": "Your input is too low. It must be at least {{min}}.", + "field-too-high": "Your input is too high. It may be at most {{max}}.", "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 the topic cloud include?", "tag-cloud-questions-all": "All questions", @@ -398,14 +404,6 @@ "add-successful": "Word added", "remove-successful": "Word removed" }, - "worker-config": { - "heading": "Extract keywords", - "label": "Choose from which questions to extract keywords using Artificial Intelligence (see »Introduction« in the footer):", - "normal": "Reanalyze all questions in the room", - "only-failed": "Only questions that have not been analyzed", - "continue": "Continue", - "cancel": "Cancel" - }, "worker-dialog": { "running": "Running", "room-name": "Room", diff --git a/src/assets/i18n/participant/de.json b/src/assets/i18n/participant/de.json index c1ab6e8d6..ffdc53cca 100644 --- a/src/assets/i18n/participant/de.json +++ b/src/assets/i18n/participant/de.json @@ -190,7 +190,9 @@ "grammar-check": "Text optimieren", "upvote": "positiv:", "downvote": "negativ:", - "questioner-name": "Selbst vergebener Name" + "questioner-name": "Selbst vergebener Name", + "brainstorming-placeholder": "Schreibe deine Wörter für das Brainstorming. Maximal {{maxWordCount}} Wörter mit jeweils maximal {{maxWordLength}} Zeichen.", + "error-comment-brainstorming": "Die Brainstorming Session erlaubt nur {{maxWordCount}} Wörter" }, "deepl": { "header": "Text optimieren", @@ -505,5 +507,13 @@ "questioner-name": "Dein Name, wenn du willst …", "a11y-questioner-name": "Hier kannst du, wenn du willst, deinen Namen eingeben. Dieser ist für alle an diesem Kommentar sichtbar.", "name-length-error": "Der Name muss zwischen 2 und 20 Zeichen lang sein." + }, + "worker-config": { + "heading": "Stichwörter extrahieren", + "label": "Wähle, aus welchen Fragen mittels Künstlicher Intelligenz (siehe »Einführung« in der Fußzeile) Stichwörter extrahiert werden sollen:", + "normal": "Alle Fragen des Raumes neu analysieren", + "only-failed": "Nur Fragen, die nicht analysiert worden sind", + "continue": "Weiter", + "cancel": "Abbrechen" } } diff --git a/src/assets/i18n/participant/en.json b/src/assets/i18n/participant/en.json index cd345280f..cf47331a3 100644 --- a/src/assets/i18n/participant/en.json +++ b/src/assets/i18n/participant/en.json @@ -199,7 +199,9 @@ "grammar-check": "Optimize text", "upvote": "upvotes:", "downvote": "downvotes:", - "questioner-name": "Self assigned name" + "questioner-name": "Self assigned name", + "brainstorming-placeholder": "Write your words for the brainstorm. Maximum {{maxWordCount}} words, each with a maximum of {{maxWordLength}} characters.", + "error-comment-brainstorming": "The brainstorming session allows only {{maxWordCount}} words" }, "deepl": { "header": "Optimize text", @@ -511,5 +513,13 @@ "questioner-name": "Your name, if you want …", "a11y-questioner-name": "Here you can enter your name if you want. This is visible for everyone at this comment.", "name-length-error": "The name must be between 2 and 20 characters." + }, + "worker-config": { + "heading": "Extract keywords", + "label": "Choose from which questions to extract keywords using Artificial Intelligence (see »Introduction« in the footer):", + "normal": "Reanalyze all questions in the room", + "only-failed": "Only questions that have not been analyzed", + "continue": "Continue", + "cancel": "Cancel" } } -- GitLab