Skip to content

Commit b994a82

Browse files
Added sample
1 parent e162186 commit b994a82

15 files changed

+485
-0
lines changed

angular.json

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
{
2+
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
3+
"version": 1,
4+
"newProjectRoot": "projects",
5+
"projects": {
6+
"ChatApp": {
7+
"projectType": "application",
8+
"schematics": {},
9+
"root": "",
10+
"sourceRoot": "src",
11+
"prefix": "app",
12+
"architect": {
13+
"build": {
14+
"builder": "@angular-devkit/build-angular:application",
15+
"options": {
16+
"outputPath": "dist/chat-app",
17+
"index": "src/index.html",
18+
"browser": "src/main.ts",
19+
"polyfills": [
20+
"zone.js"
21+
],
22+
"tsConfig": "tsconfig.app.json",
23+
"assets": [
24+
{
25+
"glob": "**/*",
26+
"input": "public"
27+
}
28+
],
29+
"styles": [
30+
"src/styles.css"
31+
],
32+
"scripts": []
33+
},
34+
"configurations": {
35+
"production": {
36+
"budgets": [
37+
{
38+
"type": "initial",
39+
"maximumWarning": "500kB",
40+
"maximumError": "1MB"
41+
},
42+
{
43+
"type": "anyComponentStyle",
44+
"maximumWarning": "4kB",
45+
"maximumError": "8kB"
46+
}
47+
],
48+
"outputHashing": "all"
49+
},
50+
"development": {
51+
"optimization": false,
52+
"extractLicenses": false,
53+
"sourceMap": true
54+
}
55+
},
56+
"defaultConfiguration": "production"
57+
},
58+
"serve": {
59+
"builder": "@angular-devkit/build-angular:dev-server",
60+
"configurations": {
61+
"production": {
62+
"buildTarget": "ChatApp:build:production"
63+
},
64+
"development": {
65+
"buildTarget": "ChatApp:build:development"
66+
}
67+
},
68+
"defaultConfiguration": "development"
69+
},
70+
"extract-i18n": {
71+
"builder": "@angular-devkit/build-angular:extract-i18n"
72+
},
73+
"test": {
74+
"builder": "@angular-devkit/build-angular:karma",
75+
"options": {
76+
"polyfills": [
77+
"zone.js",
78+
"zone.js/testing"
79+
],
80+
"tsConfig": "tsconfig.spec.json",
81+
"assets": [
82+
{
83+
"glob": "**/*",
84+
"input": "public"
85+
}
86+
],
87+
"styles": [
88+
"src/styles.css"
89+
],
90+
"scripts": []
91+
}
92+
}
93+
}
94+
}
95+
},
96+
"cli": {
97+
"analytics": false
98+
}
99+
}

package.json

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
{
2+
"name": "chat-app",
3+
"version": "0.0.0",
4+
"scripts": {
5+
"ng": "ng",
6+
"start": "ng serve",
7+
"build": "ng build",
8+
"watch": "ng build --watch --configuration development",
9+
"test": "ng test"
10+
},
11+
"private": true,
12+
"dependencies": {
13+
"@angular/common": "^19.2.0",
14+
"@angular/compiler": "^19.2.0",
15+
"@angular/core": "^19.2.0",
16+
"@angular/forms": "^19.2.0",
17+
"@angular/platform-browser": "^19.2.0",
18+
"@angular/platform-browser-dynamic": "^19.2.0",
19+
"@angular/router": "^19.2.0",
20+
"@syncfusion/ej2-angular-inputs": "^29.1.38",
21+
"@syncfusion/ej2-angular-interactive-chat": "^29.1.34",
22+
"@syncfusion/ej2-angular-notifications": "^29.1.33",
23+
"rxjs": "~7.8.0",
24+
"tslib": "^2.3.0",
25+
"zone.js": "~0.15.0"
26+
},
27+
"devDependencies": {
28+
"@angular-devkit/build-angular": "^19.2.6",
29+
"@angular/cli": "^19.2.6",
30+
"@angular/compiler-cli": "^19.2.0",
31+
"@types/jasmine": "~5.1.0",
32+
"jasmine-core": "~5.6.0",
33+
"karma": "~6.4.0",
34+
"karma-chrome-launcher": "~3.2.0",
35+
"karma-coverage": "~2.2.0",
36+
"karma-jasmine": "~5.1.0",
37+
"karma-jasmine-html-reporter": "~2.1.0",
38+
"typescript": "~5.7.2"
39+
}
40+
}

public/favicon.ico

14.7 KB
Binary file not shown.

src/app/app.component.css

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
.integration-speechToText-section {
2+
height: 550px;
3+
width: 60%;
4+
margin: 0 auto;
5+
}
6+
.integration-speechToText-section .chat_user1_avatar {
7+
background-image: url('https://ej2.syncfusion.com/angular/demos/assets/chat-ui/images/andrew.png');
8+
}
9+
.integration-speechToText-section .e-footer-template {
10+
width: 100%;
11+
}
12+
13+
.integration-speechToText-section .banner-info .e-listen-icon:before {
14+
font-size: 35px;
15+
}
16+
17+
.integration-speechToText-section .banner-info {
18+
display: flex;
19+
flex-direction: column;
20+
justify-content: center;
21+
height: 330px;
22+
text-align: center;
23+
}
24+
25+
.integration-speechToText-section .e-footer-wrapper #chatui-sendButton {
26+
width: 40px;
27+
height: 40px;
28+
font-size: 20px;
29+
border: none;
30+
background: none;
31+
cursor: pointer;
32+
}
33+
34+
.integration-speechToText-section .e-footer-wrapper #speechtotext.visible,
35+
.integration-speechToText-section .e-footer-wrapper #chatui-sendButton.visible {
36+
display: inline-block;
37+
}
38+
39+
.integration-speechToText-section .e-footer-wrapper #speechtotext,
40+
.integration-speechToText-section .e-footer-wrapper #chatui-sendButton {
41+
display: none;
42+
}
43+
44+
@media only screen and (max-width: 750px) {
45+
.integration-speechToText-section {
46+
width: 100%;
47+
}
48+
}
49+
50+
@media only screen and (max-width: 850px) {
51+
.template-chatui {
52+
width: 70%;
53+
}
54+
.template-chatui .message-suggestions {
55+
flex-direction: column;
56+
width: 80%;
57+
}
58+
.template-chatui .message-suggestions .suggestion-button {
59+
white-space: nowrap;
60+
overflow: hidden;
61+
text-overflow: ellipsis;
62+
}
63+
}
64+
65+
.integration-speechToText-section .e-footer-wrapper {
66+
display: flex;
67+
border: 1px solid #c1c1c1;
68+
padding: 5px 5px 5px 10px;
69+
margin: 5px 5px 0 5px;
70+
border-radius: 30px;
71+
}
72+
73+
.integration-speechToText-section .e-footer-wrapper .content-editor {
74+
width: 100%;
75+
overflow-y: auto;
76+
font-size: 14px;
77+
min-height: 25px;
78+
max-height: 200px;
79+
padding: 10px;
80+
scrollbar-color: #c1c1c1 transparent;
81+
}
82+
83+
.integration-speechToText-section .e-footer-wrapper .content-editor[contentEditable=true]:empty:before {
84+
content: attr(placeholder);
85+
}
86+
87+
.integration-speechToText-section .option-container {
88+
align-self: flex-end;
89+
}

src/app/app.component.html

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
<style>
2+
.control-section{
3+
margin-top: 20px;
4+
}
5+
</style>
6+
<div class="control-section integration-control-section">
7+
<div class="integration-speechToText-section">
8+
<div ejs-chatui #chatUI (created)="setupFooterButtons()" [user]="user" headerText="Andrew" headerIconCss="chat_user1_avatar"
9+
autoScrollToBottom="true" showTimeBreak="true" timeStampFormat="hh:mm a" >
10+
<ng-template #emptyChatTemplate>
11+
<div class="banner-info">
12+
<div class="e-icons e-listen-icon"></div>
13+
<h3>Speech To Text</h3>
14+
<i>Click the below mic-button to convert your voice to text.</i>
15+
</div>
16+
</ng-template>
17+
<ng-template #footerTemplate>
18+
<div class="e-footer-wrapper">
19+
<div id="chatui-footer" class="content-editor" contenteditable="true" placeholder="Click to speak or start typing..."></div>
20+
<div class="option-container">
21+
<button ejs-speechtotext #speechtotext id="speechtotext" cssClass="e-flat" (transcriptChanged)="onTranscriptChange($event)"
22+
(onStop)="toggleButtons()" (onError)="onErrorHandler($event)"></button>
23+
<button id="chatui-sendButton" class="e-assist-send e-icons" role="button"></button>
24+
</div>
25+
</div>
26+
</ng-template>
27+
<ng-template #timeBreakTemplate let-data>
28+
<div class="timebreak-template">
29+
{{ formatDate(data.messageDate) }}
30+
</div>
31+
</ng-template>
32+
</div>
33+
<ejs-toast #toast id="stt-toast" [position]="toastPosition" [target]="target" cssClass="e-toast-danger"></ejs-toast>
34+
</div>
35+
</div>

src/app/app.component.spec.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import { TestBed } from '@angular/core/testing';
2+
import { AppComponent } from './app.component';
3+
4+
describe('AppComponent', () => {
5+
beforeEach(async () => {
6+
await TestBed.configureTestingModule({
7+
imports: [AppComponent],
8+
}).compileComponents();
9+
});
10+
11+
it('should create the app', () => {
12+
const fixture = TestBed.createComponent(AppComponent);
13+
const app = fixture.componentInstance;
14+
expect(app).toBeTruthy();
15+
});
16+
17+
it(`should have the 'ChatApp' title`, () => {
18+
const fixture = TestBed.createComponent(AppComponent);
19+
const app = fixture.componentInstance;
20+
expect(app.title).toEqual('ChatApp');
21+
});
22+
23+
it('should render title', () => {
24+
const fixture = TestBed.createComponent(AppComponent);
25+
fixture.detectChanges();
26+
const compiled = fixture.nativeElement as HTMLElement;
27+
expect(compiled.querySelector('h1')?.textContent).toContain('Hello, ChatApp');
28+
});
29+
});

src/app/app.component.ts

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
import { Component, viewChild, ViewChild } from '@angular/core';
2+
import { RouterOutlet } from '@angular/router';
3+
import { ChatUIComponent, UserModel, ChatUIModule, MessageModel } from '@syncfusion/ej2-angular-interactive-chat';
4+
import { ToastComponent, ToastModule } from '@syncfusion/ej2-angular-notifications';
5+
import { SpeechToTextModule, TranscriptChangedEventArgs, ErrorEventArgs, SpeechToTextComponent} from '@syncfusion/ej2-angular-inputs';
6+
7+
@Component({
8+
selector: 'app-root',
9+
imports: [RouterOutlet, ToastModule, ChatUIModule, SpeechToTextModule],
10+
templateUrl: './app.component.html',
11+
styleUrl: './app.component.css'
12+
})
13+
export class AppComponent {
14+
title = 'ChatApp';
15+
16+
@ViewChild('toast') toastInstance!: ToastComponent;
17+
@ViewChild('chatUI') chatUIInstance!: ChatUIComponent;
18+
@ViewChild('speechtotext') speechToTextInstance!: SpeechToTextComponent;
19+
20+
private msgIdx: number = -1;
21+
22+
public toastPosition = { X: 'Right'};
23+
public target = ".integration-control-section";
24+
25+
public user: UserModel = { id: 'admin', user: 'Admin', avatarUrl: 'https://ej2.syncfusion.com/angular/demos/assets/chat-ui/images/andrew.png' };
26+
27+
ngAfterViewInit(): void {
28+
this.setupFooterButtons();
29+
}
30+
onTranscriptChange(args: TranscriptChangedEventArgs): void{
31+
const chatuiFooter = document.querySelector('#chatui-footer') as HTMLElement;
32+
if(chatuiFooter){
33+
chatuiFooter.innerText=args.transcript;
34+
}
35+
}
36+
setupFooterButtons(): void {
37+
if (this.chatUIInstance) {
38+
const chatuiElement = this.chatUIInstance.element as HTMLElement;
39+
const chatuiFooter = chatuiElement.querySelector('#chatui-footer') as HTMLElement;
40+
const sendButton = chatuiElement.querySelector('#chatui-sendButton') as HTMLElement;
41+
42+
sendButton.addEventListener('click', () => this.sendIconClicked());
43+
chatuiFooter.addEventListener('input', () => this.toggleButtons());
44+
chatuiFooter.addEventListener('keydown', (e) => this.onKeyDown(e));
45+
46+
this.toggleButtons();
47+
}
48+
}
49+
50+
onKeyDown(event: KeyboardEvent): void {
51+
if (event.key === 'Enter' && !event.shiftKey) {
52+
this.sendIconClicked();
53+
event.preventDefault(); // Prevent the default behavior of the Enter key
54+
}
55+
}
56+
57+
onErrorHandler(args: ErrorEventArgs): void{
58+
const toastContent: string = args.errorMessage;
59+
if(args.error === 'unsupported-browser') {
60+
this.speechToTextInstance.disabled = true;
61+
this.toastInstance.show( { content: toastContent, timeOut: 0});
62+
}
63+
else {
64+
this.toastInstance.show( { content: toastContent, timeOut: 5000});
65+
}
66+
}
67+
toggleButtons(): void {
68+
const chatuiFooter = document.querySelector('#chatui-footer') as HTMLElement;
69+
const sendButton = document.querySelector('#chatui-sendButton') as HTMLElement;
70+
const speechButton = document.querySelector('#speechtotext') as HTMLElement;
71+
72+
const hasText = chatuiFooter.innerText.trim() !== '';
73+
sendButton.classList.toggle('visible', hasText);
74+
speechButton.classList.toggle('visible', !hasText);
75+
76+
if (!hasText && (chatuiFooter.innerHTML.trim() === '' || chatuiFooter.innerHTML === '<br>')) {
77+
chatuiFooter.innerHTML = '';
78+
}
79+
}
80+
81+
sendIconClicked(): void {
82+
const chatuiFooter = document.querySelector('#chatui-footer') as HTMLElement;
83+
const newMsg: MessageModel = { id: 'msg-' + (this.msgIdx + 1), text: chatuiFooter.innerText, author: this.user };
84+
this.chatUIInstance.addMessage(newMsg);
85+
chatuiFooter.innerText = ''
86+
this.toggleButtons();
87+
}
88+
public formatDate(messageDate: Date): string {
89+
const today = new Date();
90+
today.setHours(0, 0, 0, 0);
91+
const dateToCompare = new Date(messageDate);
92+
dateToCompare.setHours(0, 0, 0, 0);
93+
94+
return dateToCompare.getTime() === today.getTime() ? 'Today' : messageDate.toDateString();
95+
}
96+
}

0 commit comments

Comments
 (0)