import { Injectable } from '@angular/core' ; import { ImmutableContext } from '@ngxs-labs/immer-adapter' ; import { Action , Actions , createSelector , NgxsAfterBootstrap , ofActionDispatched , Selector , State , StateContext , Store , } from '@ngxs/store' ; import { tap } from 'rxjs/operators' ; import { ChatMessage , Conversation , ModeType , SubjectType } from '../chat-message.model' ; import { ChatService } from '../services/chat.service' ; import { NlpService } from '../services/nlp.service' ; import { SpeechToTextService } from '../services/speech-to-text.service' ; import { TextToSpeechService } from '../services/text-to-speech.service' ; import { AddMessage , CloseConversation , CreateNewConversation , FetchConversations , SaveConversation , SendMessage , StartVoiceCommand , SwitchConversation , SynthesisVoice , } from './chat-box.actions' ; export class ChatBoxStateModel { conversations : Conversation [ ] ; activeConversationId : string | number ; canUseSpeechRecognition : boolean ; canUseSpeechSynthesis : boolean ; voices : SpeechSynthesisVoice [ ] ; voiceForm : { model : SynthesisVoice ; dirty : boolean ; status : string ; errors : any ; } ; activeUsersBots : any [ ] ; loading : boolean ; } dynamic @ State < ChatBoxStateModel > ( { name : 'chatbox' , defaults : { conversations : [ ] , activeConversationId : null , canUseSpeechRecognition : false , canUseSpeechSynthesis : false , voices : [ ] , voiceForm : { model : { voice : null , volume : 1 , rate : 1 , pitch : 1 , } , dirty : false , status : 'VALID' , errors : { } , } , activeUsersBots : [ ] , loading : false , } , } ) @ Injectable ( { providedIn : 'root' , } ) export class ChatBoxState implements NgxsAfterBootstrap { constructor ( private nlp , private stt , private tts , private chat , private store , private actions$ ) { this . actions$ . pipe ( ofActionDispatched ( AddMessage ) ) . subscribe ( ( action ) => { switch ( action . payload . message . to . type ) { case SubjectType . BOT : return this . nlp . process ( action . payload . message . content ) . then ( ( speech ) => { store . dispatch ( new AddMessage ( { conversationId : action . payload . conversationId , message : ChatMessage . fromBotMessage ( speech ) , } ) ) ; if ( action . payload . message . mode === ModeType . SPEAK ) { this . tts . synthesisVoice ( speech , this . store . selectSnapshot ( ChatBoxState . getVoicePreference ) ) ; } } ) ; case SubjectType . USER : case SubjectType . ADMIN : case SubjectType . GROUP : case SubjectType . ALL : break ; default : break ; } } ) ; } @ Selector ( ) public static canUseSpeechRecognition ( state ) { return state . canUseSpeechRecognition ; } @ Selector ( ) public static canUseSpeechSynthesis ( state ) { return state . canUseSpeechSynthesis ; } @ Selector ( ) public static loading ( state ) { return state . loading ; } @ Selector ( ) static getConversations ( state ) { return state . conversations ; } @ Selector ( ) static getActiveConversation ( state ) { return state . conversations [ state . conversations . findIndex ( ( con ) => con . id === state . activeConversationId ) ] ; } @ Selector ( ) static getConversationById ( id ) { return createSelector ( [ ChatBoxState ] , ( state ) => { return state . conversations . find ( ( con ) => con . id === id ) ; } ) ; } @ Selector ( ) static getVoices ( state ) { return state . voices ; } @ Selector ( ) static getVoicePreference ( state ) { return state . voiceForm . model ; } async ngxsAfterBootstrap ( { patchState , dispatch } ) { console . log ( 'ChatBoxState initialized, setting defaults' ) ; if ( this . tts . canUseSpeechSynthesis ) { const voices = await this . tts . getVoiceList ( ) ; patchState ( { canUseSpeechRecognition : this . stt . canUseSpeechRecognition , canUseSpeechSynthesis : this . tts . canUseSpeechSynthesis , voices , voiceForm : { model : { voice : voices [ 48 ] , volume : 1 , rate : 1 , pitch : 1 , } , dirty : false , status : 'VALID' , errors : { } , } , } ) ; } dispatch ( new FetchConversations ( ) ) ; } @ Action ( FetchConversations ) fetchConversations ( ctx ) { console . log ( 'fetching open conversations' ) ; } @ ImmutableContext ( ) @ Action ( CreateNewConversation ) createConversation ( ctx ) { const newConversation = new Conversation ( 'payload.conversationId' ) ; ctx . setState ( ( state ) => { state . conversations . push ( newConversation ) ; state . activeConversationId = newConversation . id ; return state ; } ) ; } @ ImmutableContext ( ) @ Action ( SwitchConversation ) switchConversation ( ctx , { payload } ) { const state = ctx . getState ( ) ; const switchedConversation = state . conversations [ state . conversations . findIndex ( ( con ) => con . id === state . activeConversationId ) ] ; if ( switchedConversation ) { ctx . setState ( ( state2 ) => { state2 . activeConversationId = switchedConversation . id ; return state2 ; } ) ; } } @ Action ( SaveConversation ) saveConversation ( ctx , { payload } ) { console . log ( `saving conversation ${ payload . conversationId }` ) ; } @ ImmutableContext ( ) @ Action ( CloseConversation ) closeConversation ( ctx , { payload } ) { console . log ( `close conversation ${ payload . conversationId }` ) ; const closingConversation = ctx . getState ( ) . conversations . find ( ( con ) => con . id === payload . conversationId ) ; ctx . dispatch ( new SaveConversation ( { conversationId : payload . conversationId } ) ) . pipe ( tap ( ( _ ) => { ctx . setState ( ( state ) => { state . conversations . splice ( state . conversations . findIndex ( ( con ) => con . id === payload . conversationId ) , 1 ) ; state . activeConversationId = state . conversations [ state . conversations . length - 1 ] . id ; return state ; } ) ; } ) ) ; } @ ImmutableContext ( ) @ Action ( AddMessage , { cancelUncompleted : true } ) addMessage ( ctx , { payload } ) { ctx . setState ( ( state ) => { const convIdx = state . conversations . findIndex ( ( con ) => con . id === payload . conversationId ) ; const conv = state . conversations [ convIdx ] ; state . conversations [ convIdx ] = new Conversation ( conv . id , [ ... conv . messages , payload . message ] ) ; state . activeConversationId = state . conversations [ convIdx ] . id ; return state ; } ) ; } @ Action ( SendMessage , { cancelUncompleted : true } ) async sendMessage ( ctx , { payload } ) { ctx . dispatch ( new AddMessage ( { conversationId : ctx . getState ( ) . activeConversationId , message : ChatMessage . fromUserMessage ( payload . message , ModeType . TYPE ) , } ) ) ; } @ Action ( StartVoiceCommand , { cancelUncompleted : true } ) async startVoiceCommand ( ctx ) { const message = await this . stt . getVoiceMessage ( ) ; ctx . dispatch ( new AddMessage ( { conversationId : ctx . getState ( ) . activeConversationId , message : ChatMessage . fromUserMessage ( message , ModeType . SPEAK ) , } ) ) ; } }