Merge pull request #26 from avatao-content/messagequeue

Messagequeue
This commit is contained in:
therealkrispet 2018-06-26 17:37:18 +02:00 committed by GitHub
commit 2071e81bad
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 223 additions and 40 deletions

View File

@ -97,12 +97,40 @@ The message format used:
"data":
{
"originator": ...,
"timestamp": ...,
"message": ...
}
}
```
This component can be used as a simple chatbot as well.
You can provide a list of messages to queue and they will be automatically displayed one after the other.
There is a wait time between each message so that the user can properly read them.
This wait time is calculated from the length of the last message, and can be configured using the `messages.messageQueueWPM` key in `config.ts`.
You can queue messages like so:
```
{
"key": "queueMessages",
"data":
{
"messages":
[
{
"originator": ...
"message": ...
},
{
"originator": ...
"message": ...
},
...
]
}
}
```
You can use the `MessageSender` class to send messages from the TFW server or event handlers written in Python.
### Web customisable component

View File

@ -38,7 +38,8 @@ export const config = {
},
messages: {
route: 'messages',
showNextButton: false
showNextButton: false,
messageQueueWPM: 150
},
console: {
route: 'console',

View File

@ -4,8 +4,10 @@
<div [attr.class]="layout">
<div class="tfw-grid-main-components">
<div class="tfw-header"><app-header></app-header></div>
<div [ngClass]="{'hide-attribute': hideMessages}" class="tfw-messages">
<app-messages></app-messages>
<div [ngClass]="{'hide-attribute': hideMessages}"
class="tfw-messages"
#tfwmessages>
<app-messages (newMessageEvent)="scrollMessagesToBottom()"></app-messages>
</div>
<div class="tfw-web tao-grid-top-left"
[ngClass]="{'deploy-blur': deploying}">

View File

@ -58,6 +58,11 @@
}
}
.tfw-messages::-webkit-scrollbar {
width: 0px;
background: transparent;
}
.tfw-ide {
background-color: #1e1e1e; // vscode dark theme
}

View File

@ -20,6 +20,7 @@ export class DashboardComponent implements OnInit, OnDestroy {
deploying = false;
deploymentNotificationSubscription: Subscription;
@ViewChild('webiframe') webiframe: ElementRef;
@ViewChild('tfwmessages') messages: ElementRef;
layout: string = config.dashboard.currentLayout;
hideMessages: boolean = config.dashboard.hideMessages;
@ -125,4 +126,11 @@ export class DashboardComponent implements OnInit, OnDestroy {
this.selectTerminalMenuItem('console');
}
}
scrollMessagesToBottom(): void {
const element = this.messages.nativeElement;
// This must be done in the Angular event loop to avoid messing up
// change detection (not in the template like ConsoleComponent does)
element.scrollTop = element.scrollHeight;
}
}

View File

@ -0,0 +1,7 @@
// Copyright (C) 2018 Avatao.com Innovative Learning Kft.
// All Rights Reserved. See LICENSE file for details.
export class MessageMetadata {
originator: string;
timestamp?: Date;
}

View File

@ -0,0 +1,8 @@
// Copyright (C) 2018 Avatao.com Innovative Learning Kft.
// All Rights Reserved. See LICENSE file for details.
import { MessagesMessage } from './messages-message';
export class MessageQueueMessage {
messages: Array<MessagesMessage>;
}

View File

@ -1,8 +1,8 @@
// Copyright (C) 2018 Avatao.com Innovative Learning Kft.
// All Rights Reserved. See LICENSE file for details.
export class MessagesMessage {
originator: string;
timestamp: Date;
import { MessageMetadata } from './message-metadata';
export class MessagesMessage extends MessageMetadata {
message: string;
}

View File

@ -2,11 +2,9 @@
All Rights Reserved. See LICENSE file for details. -->
<div class="tfw-messages-main">
<div class="tfw-grid-messages-header">
<div class="tao-grid-top-left"><span>Instructions</span></div>
<div class="tao-grid-center-right"><button *ngIf="showNextButton" (click)="stepFSM()" class="tao-btn-rainbow">Next</button></div>
</div>
<div class="tfw-grid-message" *ngFor="let message of messages.slice().reverse()">
<div class="tfw-grid-message"
*ngFor="let message of messages.slice(); let last = last"
[class.highlighted-message]="last">
<div class="tfw-grid-message-header">
<img class="tao-grid-center-left" src="images/avataobot.svg"/>
<div class="tao-grid-center-left originator">{{message.originator}}</div>
@ -14,4 +12,16 @@
</div>
<div [innerHtml]="message.message"></div>
</div>
<div *ngIf="messageInQueue" class="tfw-grid-message jumping-circle-container">
<div class="jumping-circle" id="jc1"></div>
<div class="jumping-circle" id="jc2"></div>
<div class="jumping-circle" id="jc3"></div>
</div>
<div class="tfw-next-button">
<button *ngIf="showNextButton"
(click)="stepFSM()"
class="tao-btn-rainbow">
Next
</button>
</div>
</div>

View File

@ -3,18 +3,8 @@
@import "../../assets/scss/variables.scss";
.tfw-messages-main {
.tfw-grid-messages-header {
display: grid;
grid-template-columns: 1fr 1fr;
margin-bottom: $small;
span {
color: $tao-blue-500;
font-weight: 500;
font-size: $font-size-h3;
}
}
.tfw-next-button {
text-align: center;
}
.tfw-grid-message {
@ -28,6 +18,51 @@
padding: $tiny;
font-size: $font-size-base;
margin-bottom: $hair;
animation-name: inflate;
animation-duration: 0.5s;
animation-timing-function: cubic-bezier(0.01, 0.1, 0, 1);
}
.highlighted-message {
box-shadow: 0px 0px 3px 0px rgba($tao-gray-300,0.65);
}
@keyframes inflate {
0% { transform: scale(0,0); }
100% { transform: scale(1,1); }
}
.jumping-circle-container {
display: flex;
padding-top: 1.3em;
padding-bottom: 1em;
}
.jumping-circle {
width: 0.35em;
height: 0.35em;
border-radius: 50%;
background-color: gray;
margin-top: 0.3em;
margin-left: 0.3em;
animation-name: float;
animation-duration: 1.7s;
animation-timing-function: ease-in-out;
animation-iteration-count: infinite;
}
#jc2 { animation-delay: 0.2s; }
#jc3 { animation-delay: 0.45s; }
@keyframes float {
0% { transform: translateY(0em); }
30% { transform: translateY(-0.5em); }
60% { transform: translateY(0.2em); }
80% { transform: translateY(0em); }
90% { transform: translateY(-0.5em); }
100% { transform: translateY(0em); }
}
.tfw-grid-message-header {

View File

@ -1,7 +1,7 @@
// Copyright (C) 2018 Avatao.com Innovative Learning Kft.
// All Rights Reserved. See LICENSE file for details.
import { ChangeDetectorRef, Component, OnInit } from '@angular/core';
import { ChangeDetectorRef, Component, OnInit, EventEmitter, Output } from '@angular/core';
import { MarkdownService } from '../services/markdown.service';
import { WebSocketService } from '../services/websocket.service';
@ -9,6 +9,8 @@ import { MessagesMessage } from '../message-types/messages-message';
import { MessagesControlCommand } from '../message-types/messages-control-command';
import { config } from '../config';
import { CommandMessage } from '../message-types/command-message';
import { MessageQueueMessage } from '../message-types/message-queue-message';
import { Subject, Observer, BehaviorSubject } from 'rxjs';
@Component({
selector: 'app-messages',
@ -16,7 +18,12 @@ import { CommandMessage } from '../message-types/command-message';
styleUrls: ['./messages.component.scss']
})
export class MessagesComponent implements OnInit {
@Output() newMessageEvent: EventEmitter<any> = new EventEmitter();
newMessage: Subject<MessagesMessage> = new Subject<MessagesMessage>();
messageInQueue = true;
messages: MessagesMessage[] = [];
messageQueueAttender: MessageQueueAttender;
showNextButton: boolean = config.messages.showNextButton;
command_handlers = {
@ -27,23 +34,50 @@ export class MessagesComponent implements OnInit {
private markdownService: MarkdownService,
private websocketService: WebSocketService,
private changeDetectorRef: ChangeDetectorRef
) {}
ngOnInit() {
this.websocketService.connect();
this.websocketService.observeKey<MessagesMessage>('message').subscribe(
(event) => {
this.messages.push(event.data);
event.data.message = this.convert(event.data.message);
this.changeDetectorRef.detectChanges();
});
this.websocketService.observeKey<CommandMessage>('messagecontrol').subscribe(
(event) => {
this.command_handlers[event.data.command](event.data);
});
) {
this.messageQueueAttender = new MessageQueueAttender(this.newMessage);
}
convert(text: string) {
ngOnInit() {
this.newMessage.subscribe(
(message) => {
this.writeMessage(message);
this.newMessageEvent.emit();
});
this.messageQueueAttender.messageInQueue.subscribe(
(value) => this.messageInQueue = value
);
this.websocketService.connect();
this.websocketService.observeKey<MessagesMessage>('message').subscribe(
(event) => this.newMessage.next(event.data)
);
this.websocketService.observeKey<MessageQueueMessage>('queueMessages').subscribe(
(event) => this.handleQueueMessage(event.data)
);
this.websocketService.observeKey<CommandMessage>('messagecontrol').subscribe(
(event) => this.command_handlers[event.data.command](event.data)
);
}
writeMessage(message: MessagesMessage) {
this.transformMessage(message);
this.messages.push(message);
this.changeDetectorRef.detectChanges();
}
transformMessage(message: MessagesMessage) {
message.message = this.convertMarkdownToHTML(message.message);
if (!message.timestamp) {
message.timestamp = new Date();
}
}
handleQueueMessage(data: MessageQueueMessage) {
this.messageQueueAttender.queueMessages(data.messages);
}
convertMarkdownToHTML(text: string) {
return this.markdownService.convertToHtml(text);
}
@ -55,3 +89,48 @@ export class MessagesComponent implements OnInit {
this.websocketService.sendJSON({key: '', trigger: 'step_next'});
}
}
class MessageQueueAttender {
public messageInQueue = new BehaviorSubject<boolean>(false);
private readonly charPerSecond: number;
private lastMessageLength = 0;
private queue: MessagesMessage[] = [];
constructor(private messageEmitter: Observer<MessagesMessage>, wordPerMinute: number = config.messages.messageQueueWPM) {
const charPerMinute = wordPerMinute * 5;
this.charPerSecond = charPerMinute / 60;
}
queueMessages(messages: Array<MessagesMessage>) {
this.queue = this.queue.concat(messages);
this.attendQueue();
}
private processMessage() {
if (this.queue.length > 0) {
const lastMessage = this.queue.shift();
this.lastMessageLength = lastMessage.message.length;
this.messageEmitter.next(lastMessage);
}
if (this.queue.length === 0) {
this.lastMessageLength = 0;
this.messageInQueue.next(false);
}
}
private attendQueue() {
if (this.queue.length > 0) {
this.messageInQueue.next(true);
const timeoutSeconds = this.lastMessageLength / this.charPerSecond;
setTimeout(
() => {
this.processMessage();
this.attendQueue();
},
timeoutSeconds * 1000
);
}
}
}