mirror of
https://github.com/avatao-content/frontend-tutorial-framework
synced 2025-04-03 11:52:40 +00:00
commit
2071e81bad
30
README.md
30
README.md
@ -97,12 +97,40 @@ The message format used:
|
|||||||
"data":
|
"data":
|
||||||
{
|
{
|
||||||
"originator": ...,
|
"originator": ...,
|
||||||
"timestamp": ...,
|
|
||||||
"message": ...
|
"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.
|
You can use the `MessageSender` class to send messages from the TFW server or event handlers written in Python.
|
||||||
|
|
||||||
### Web – customisable component
|
### Web – customisable component
|
||||||
|
@ -38,7 +38,8 @@ export const config = {
|
|||||||
},
|
},
|
||||||
messages: {
|
messages: {
|
||||||
route: 'messages',
|
route: 'messages',
|
||||||
showNextButton: false
|
showNextButton: false,
|
||||||
|
messageQueueWPM: 150
|
||||||
},
|
},
|
||||||
console: {
|
console: {
|
||||||
route: 'console',
|
route: 'console',
|
||||||
|
@ -4,8 +4,10 @@
|
|||||||
<div [attr.class]="layout">
|
<div [attr.class]="layout">
|
||||||
<div class="tfw-grid-main-components">
|
<div class="tfw-grid-main-components">
|
||||||
<div class="tfw-header"><app-header></app-header></div>
|
<div class="tfw-header"><app-header></app-header></div>
|
||||||
<div [ngClass]="{'hide-attribute': hideMessages}" class="tfw-messages">
|
<div [ngClass]="{'hide-attribute': hideMessages}"
|
||||||
<app-messages></app-messages>
|
class="tfw-messages"
|
||||||
|
#tfwmessages>
|
||||||
|
<app-messages (newMessageEvent)="scrollMessagesToBottom()"></app-messages>
|
||||||
</div>
|
</div>
|
||||||
<div class="tfw-web tao-grid-top-left"
|
<div class="tfw-web tao-grid-top-left"
|
||||||
[ngClass]="{'deploy-blur': deploying}">
|
[ngClass]="{'deploy-blur': deploying}">
|
||||||
|
@ -58,6 +58,11 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.tfw-messages::-webkit-scrollbar {
|
||||||
|
width: 0px;
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
.tfw-ide {
|
.tfw-ide {
|
||||||
background-color: #1e1e1e; // vscode dark theme
|
background-color: #1e1e1e; // vscode dark theme
|
||||||
}
|
}
|
||||||
|
@ -20,6 +20,7 @@ export class DashboardComponent implements OnInit, OnDestroy {
|
|||||||
deploying = false;
|
deploying = false;
|
||||||
deploymentNotificationSubscription: Subscription;
|
deploymentNotificationSubscription: Subscription;
|
||||||
@ViewChild('webiframe') webiframe: ElementRef;
|
@ViewChild('webiframe') webiframe: ElementRef;
|
||||||
|
@ViewChild('tfwmessages') messages: ElementRef;
|
||||||
|
|
||||||
layout: string = config.dashboard.currentLayout;
|
layout: string = config.dashboard.currentLayout;
|
||||||
hideMessages: boolean = config.dashboard.hideMessages;
|
hideMessages: boolean = config.dashboard.hideMessages;
|
||||||
@ -125,4 +126,11 @@ export class DashboardComponent implements OnInit, OnDestroy {
|
|||||||
this.selectTerminalMenuItem('console');
|
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.
|
// Copyright (C) 2018 Avatao.com Innovative Learning Kft.
|
||||||
// All Rights Reserved. See LICENSE file for details.
|
// All Rights Reserved. See LICENSE file for details.
|
||||||
|
|
||||||
export class MessagesMessage {
|
import { MessageMetadata } from './message-metadata';
|
||||||
originator: string;
|
|
||||||
timestamp: Date;
|
export class MessagesMessage extends MessageMetadata {
|
||||||
message: string;
|
message: string;
|
||||||
}
|
}
|
||||||
|
@ -2,11 +2,9 @@
|
|||||||
All Rights Reserved. See LICENSE file for details. -->
|
All Rights Reserved. See LICENSE file for details. -->
|
||||||
|
|
||||||
<div class="tfw-messages-main">
|
<div class="tfw-messages-main">
|
||||||
<div class="tfw-grid-messages-header">
|
<div class="tfw-grid-message"
|
||||||
<div class="tao-grid-top-left"><span>Instructions</span></div>
|
*ngFor="let message of messages.slice(); let last = last"
|
||||||
<div class="tao-grid-center-right"><button *ngIf="showNextButton" (click)="stepFSM()" class="tao-btn-rainbow">Next</button></div>
|
[class.highlighted-message]="last">
|
||||||
</div>
|
|
||||||
<div class="tfw-grid-message" *ngFor="let message of messages.slice().reverse()">
|
|
||||||
<div class="tfw-grid-message-header">
|
<div class="tfw-grid-message-header">
|
||||||
<img class="tao-grid-center-left" src="images/avataobot.svg"/>
|
<img class="tao-grid-center-left" src="images/avataobot.svg"/>
|
||||||
<div class="tao-grid-center-left originator">{{message.originator}}</div>
|
<div class="tao-grid-center-left originator">{{message.originator}}</div>
|
||||||
@ -14,4 +12,16 @@
|
|||||||
</div>
|
</div>
|
||||||
<div [innerHtml]="message.message"></div>
|
<div [innerHtml]="message.message"></div>
|
||||||
</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>
|
</div>
|
||||||
|
@ -3,18 +3,8 @@
|
|||||||
|
|
||||||
@import "../../assets/scss/variables.scss";
|
@import "../../assets/scss/variables.scss";
|
||||||
|
|
||||||
.tfw-messages-main {
|
.tfw-next-button {
|
||||||
.tfw-grid-messages-header {
|
text-align: center;
|
||||||
display: grid;
|
|
||||||
grid-template-columns: 1fr 1fr;
|
|
||||||
margin-bottom: $small;
|
|
||||||
|
|
||||||
span {
|
|
||||||
color: $tao-blue-500;
|
|
||||||
font-weight: 500;
|
|
||||||
font-size: $font-size-h3;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.tfw-grid-message {
|
.tfw-grid-message {
|
||||||
@ -28,6 +18,51 @@
|
|||||||
padding: $tiny;
|
padding: $tiny;
|
||||||
font-size: $font-size-base;
|
font-size: $font-size-base;
|
||||||
margin-bottom: $hair;
|
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 {
|
.tfw-grid-message-header {
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
// Copyright (C) 2018 Avatao.com Innovative Learning Kft.
|
// Copyright (C) 2018 Avatao.com Innovative Learning Kft.
|
||||||
// All Rights Reserved. See LICENSE file for details.
|
// 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 { MarkdownService } from '../services/markdown.service';
|
||||||
import { WebSocketService } from '../services/websocket.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 { MessagesControlCommand } from '../message-types/messages-control-command';
|
||||||
import { config } from '../config';
|
import { config } from '../config';
|
||||||
import { CommandMessage } from '../message-types/command-message';
|
import { CommandMessage } from '../message-types/command-message';
|
||||||
|
import { MessageQueueMessage } from '../message-types/message-queue-message';
|
||||||
|
import { Subject, Observer, BehaviorSubject } from 'rxjs';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-messages',
|
selector: 'app-messages',
|
||||||
@ -16,7 +18,12 @@ import { CommandMessage } from '../message-types/command-message';
|
|||||||
styleUrls: ['./messages.component.scss']
|
styleUrls: ['./messages.component.scss']
|
||||||
})
|
})
|
||||||
export class MessagesComponent implements OnInit {
|
export class MessagesComponent implements OnInit {
|
||||||
|
@Output() newMessageEvent: EventEmitter<any> = new EventEmitter();
|
||||||
|
newMessage: Subject<MessagesMessage> = new Subject<MessagesMessage>();
|
||||||
|
messageInQueue = true;
|
||||||
|
|
||||||
messages: MessagesMessage[] = [];
|
messages: MessagesMessage[] = [];
|
||||||
|
messageQueueAttender: MessageQueueAttender;
|
||||||
showNextButton: boolean = config.messages.showNextButton;
|
showNextButton: boolean = config.messages.showNextButton;
|
||||||
|
|
||||||
command_handlers = {
|
command_handlers = {
|
||||||
@ -27,23 +34,50 @@ export class MessagesComponent implements OnInit {
|
|||||||
private markdownService: MarkdownService,
|
private markdownService: MarkdownService,
|
||||||
private websocketService: WebSocketService,
|
private websocketService: WebSocketService,
|
||||||
private changeDetectorRef: ChangeDetectorRef
|
private changeDetectorRef: ChangeDetectorRef
|
||||||
) {}
|
) {
|
||||||
|
this.messageQueueAttender = new MessageQueueAttender(this.newMessage);
|
||||||
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);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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);
|
return this.markdownService.convertToHtml(text);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -55,3 +89,48 @@ export class MessagesComponent implements OnInit {
|
|||||||
this.websocketService.sendJSON({key: '', trigger: 'step_next'});
|
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…
x
Reference in New Issue
Block a user