mirror of
https://github.com/avatao-content/frontend-tutorial-framework
synced 2024-12-05 02:21:31 +00:00
commit
2071e81bad
30
README.md
30
README.md
@ -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
|
||||
|
@ -38,7 +38,8 @@ export const config = {
|
||||
},
|
||||
messages: {
|
||||
route: 'messages',
|
||||
showNextButton: false
|
||||
showNextButton: false,
|
||||
messageQueueWPM: 150
|
||||
},
|
||||
console: {
|
||||
route: 'console',
|
||||
|
@ -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}">
|
||||
|
@ -58,6 +58,11 @@
|
||||
}
|
||||
}
|
||||
|
||||
.tfw-messages::-webkit-scrollbar {
|
||||
width: 0px;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.tfw-ide {
|
||||
background-color: #1e1e1e; // vscode dark theme
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
7
src/app/message-types/message-metadata.ts
Normal file
7
src/app/message-types/message-metadata.ts
Normal 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;
|
||||
}
|
8
src/app/message-types/message-queue-message.ts
Normal file
8
src/app/message-types/message-queue-message.ts
Normal 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>;
|
||||
}
|
@ -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;
|
||||
}
|
||||
|
@ -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>
|
||||
|
@ -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 {
|
||||
|
@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user