Home > Software engineering >  RxJs: Implementing Chatbot delayed messages
RxJs: Implementing Chatbot delayed messages

Time:09-03

I am working on chat application where i need to display bot messages one-by-one with some delay between them, such that it gives an impression that bot is typing instead of throwing all message together. I was trying this behaviour with RxJS but could not achieve the desired output.

Stackblitz Link

query(): Observable<IChatResponse> {
    const response = {
      messages: [
        {
          text: 'Please give a valid domain name',
        },
        {
          text: 'What domain do you want?',
        },
        {
          text: 'Some other messages...',
        },
      ],
    };

    return of(response).pipe(
      switchMap((response: any) => this.convertToStream(response))
    );
  }

  convertToStream(data: any): Observable<IChatResponse> {
    let count = 0;
    const messageDelayFn = (sme, idx): Observable<any> => {
      const loaderStart$ = of(null).pipe(
        tap((_) => console.log('idx ', idx)),
        delay(500 * idx),
        tap((_) => {
          this.loading$.next(true);
        })
      );
      const loaderStop$ = of(null).pipe(
        delay(1000 * idx),
        tap((_) => {
          this.loading$.next(false);
        })
      );

      const message$ = of(sme);
      return concat(loaderStart$, loaderStop$, message$).pipe(share());
    };

    const transformedObservable = of(data).pipe(
      map((chat) => {
        return {
          ...chat,
          messages: chat.messages.reduce((acc: Observable<any>[], message) => {
            return [...acc, messageDelayFn(message,   count)];
          }, []),
        };
      })
    );

    return transformedObservable;
  }

Behaviour i am expecting is something like this --,

  • starts with loader for 500ms
  • loader stops
  • emits first message
  • again loader starts for 500ms (before emitting second message)
  • loader stops
  • emits second message
  • ...and so on

CodePudding user response:

Here is my solution. Using subject combined with concat, since concat will execute the next observable only after the previous observable completes, I have added a delay and it seems to work as per the requirements.

service

import { Injectable } from '@angular/core';
import {
  concat,
  map,
  Observable,
  of,
  share,
  switchMap,
  tap,
  delay,
  Subject,
  BehaviorSubject,
  concatMap,
} from 'rxjs';
import { IChatResponse } from './app.component';

@Injectable({ providedIn: 'root' })
export default class AppService {
  loading$ = new Subject<boolean>();
  get loadingStream(): Observable<boolean> {
    return this.loading$.asObservable();
  }

  query(): Observable<any> {
    const subject = new BehaviorSubject(['loading']);
    const response = {
      messages: [
        {
          text: 'Please give a valid domain name',
        },
        {
          text: 'What domain do you want?',
        },
        {
          text: 'Some other messages...',
        },
      ],
    };
    const observables$ = response.messages.map((x) => {
      return of(x.text).pipe(delay(3000));
    });

    return concat(...observables$).pipe(
      tap((x) => {
        const arr = subject.getValue();
        arr[arr.length - 1] = x;
        subject.next(
          arr.length === response.messages.length ? arr : [...arr, 'loading']
        );
      }),
      concatMap(() => subject.asObservable())
    );
  }
}

ts

import { Component, OnDestroy, OnInit, VERSION } from '@angular/core';
import { Observable, Subscription } from 'rxjs';
import AppService from './app.service';

export interface IChatResponse {
  messages: Observable<IMessage>[];
}
interface IMessage {
  text: string;
}

@Component({
  selector: 'my-app',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css'],
})
export class AppComponent implements OnInit, OnDestroy {
  name = 'Angular '   VERSION.major;
  chatMessages$: any;
  subscription = new Subscription();
  loader$: Observable<boolean>;
  constructor(private appService: AppService) {
    this.loader$ = this.appService.loadingStream;
  }

  ngOnInit(): void {
    this.chatMessages$ = <any>this.appService.query();
  }

  ngOnDestroy(): void {
    this.subscription.unsubscribe();
  }
}

html

<hello name="{{ name }}"></hello>
<p *ngFor="let message of chatMessages$ | async">
  <ng-container [ngSwitch]="message">
    <ng-container *ngSwitchCase="'loading'">
      <svg
        xmlns="http://www.w3.org/2000/svg"
        xmlns:xlink="http://www.w3.org/1999/xlink"
        style="margin:auto;background:#fff;display:block;"
        width="200px"
        height="200px"
        viewBox="0 0 100 100"
        preserveAspectRatio="xMidYMid"
      >
        <g transform="translate(20 50)">
          <circle cx="0" cy="0" r="6" fill="#e15b64">
            <animateTransform
              attributeName="transform"
              type="scale"
              begin="-0.375s"
              calcMode="spline"
              keySplines="0.3 0 0.7 1;0.3 0 0.7 1"
              values="0;1;0"
              keyTimes="0;0.5;1"
              dur="1s"
              repeatCount="indefinite"
            ></animateTransform>
          </circle>
        </g>
        <g transform="translate(40 50)">
          <circle cx="0" cy="0" r="6" fill="#f8b26a">
            <animateTransform
              attributeName="transform"
              type="scale"
              begin="-0.25s"
              calcMode="spline"
              keySplines="0.3 0 0.7 1;0.3 0 0.7 1"
              values="0;1;0"
              keyTimes="0;0.5;1"
              dur="1s"
              repeatCount="indefinite"
            ></animateTransform>
          </circle>
        </g>
        <g transform="translate(60 50)">
          <circle cx="0" cy="0" r="6" fill="#abbd81">
            <animateTransform
              attributeName="transform"
              type="scale"
              begin="-0.125s"
              calcMode="spline"
              keySplines="0.3 0 0.7 1;0.3 0 0.7 1"
              values="0;1;0"
              keyTimes="0;0.5;1"
              dur="1s"
              repeatCount="indefinite"
            ></animateTransform>
          </circle>
        </g>
        <g transform="translate(80 50)">
          <circle cx="0" cy="0" r="6" fill="#81a3bd">
            <animateTransform
              attributeName="transform"
              type="scale"
              begin="0s"
              calcMode="spline"
              keySplines="0.3 0 0.7 1;0.3 0 0.7 1"
              values="0;1;0"
              keyTimes="0;0.5;1"
              dur="1s"
              repeatCount="indefinite"
            ></animateTransform>
          </circle>
        </g>
      </svg>
    </ng-container>
    <ng-container *ngSwitchDefault>{{ message }}</ng-container>
  </ng-container>
</p>

forked stackblitz

CodePudding user response:

service

export default class AppService {
  query(): Observable<IMessage> {
    const response = {
      messages: [
        {
          text: 'Please give a valid domain name',
        },
        {
          text: 'What domain do you want?',
        },
        {
          text: 'Some other messages...',
        },
      ],
    };
    return from(response.messages);
  }
}

component

export class AppComponent {
  delayTime$ = timer(1000).pipe(ignoreElements());

  text$ = this.appService.query().pipe(
    concatMap(({ text }) => {
      const text$ = of(text).pipe(delay(1000), share());
      return concat(this.delayTime$, of(text$), text$.pipe(ignoreElements()));
    }),
    scan((arr, v) => [...arr, v], [])
  );

  constructor(private appService: AppService) {}
}

html

<div *ngFor="let item of text$ | async">
  <div >
    <span *ngIf="item | async as item; else loading">
      {{ item }}
    </span>
    <ng-template #loading
      ><img src="https://c.tenor.com/VS20soWAM9AAAAAi/loading.gif" width="30"
    /></ng-template>
  </div>
</div>

https://stackblitz.com/edit/angular-ivy-ot6dcr

CodePudding user response:

Here's a function that matches your spec. You can try running this yourself and then hopefully adapt it to your use case as needed.

function query(): Observable<string> {
  const response = {
    messages: [
      {
        text: 'Please give a valid domain name',
      },
      {
        text: 'What domain do you want?',
      },
      {
        text: 'Some other messages...',
      },
    ],
  };

  return concat(...response.messages.map(({text}) => 
    timer(500).pipe(map(_ => text))
  ));
}

query().subscribe(console.log);
  • Related