import { Component, Inject, OnDestroy, PLATFORM_ID, ChangeDetectorRef } from '@angular/core';
import { ApiService } from './api.service';
import { FormsModule } from '@angular/forms';
import { CommonModule } from '@angular/common';
import { Router, RouterModule } from '@angular/router';
import { isPlatformBrowser } from '@angular/common';
import { ViewChild, ElementRef } from '@angular/core';
import { Renderer2 } from '@angular/core';
import { Subscription } from 'rxjs';
@Component({
selector: 'app-chat',
standalone: true,
imports: [FormsModule, CommonModule, RouterModule],
templateUrl: './chat.component.html',
styleUrl: './chat.component.css'
})
export class ChatComponent implements OnDestroy {
showQuestions: boolean = false;
userInput: string = '';
messages: { from: string, text: string, timestamp: string; isPlaying?: boolean }[] = [];
isTyping: boolean = false;
@ViewChild('chatBox') chatBox!: ElementRef;
isLoadingSpeech: boolean = false;
selectedVoice: SpeechSynthesisVoice | null = null;
errorMessage: string = "";
recognition: any;
speechSynthesisInstance: SpeechSynthesisUtterance | null = null;
isListening: boolean = false;
isProcessingSpeech: boolean = false;
isSpeaking: boolean = false;
isAudioPaused: boolean = false;
isInputValid: boolean = false;
suggestions: string[] = [];
showMicPopup: boolean = false;
isSubmitting: boolean = false;
private responseSub?: Subscription;
private lastFullAiText: string = '';
ngAfterViewChecked() {
setTimeout(() => {
this.scrollToBottom();
}, 100);
}
private scrollToBottom(): void {
try {
this.chatBox.nativeElement.scrollTop = this.chatBox.nativeElement.scrollHeight;
} catch (err) { }
}
constructor(
private apiService: ApiService,
private cdr: ChangeDetectorRef,
@Inject(PLATFORM_ID,) private platformId: object,
private router: Router,
private renderer: Renderer2
) {
window.speechSynthesis.onvoiceschanged = () => {
console.log("Available Voices:", window.speechSynthesis.getVoices());
};
if (isPlatformBrowser(this.platformId)) {
const SpeechRecognition = (window as any).SpeechRecognition || (window as any).webkitSpeechRecognition;
if (SpeechRecognition) {
this.recognition = new SpeechRecognition();
this.recognition.continuous = false;
this.recognition.lang = 'en-US';
this.recognition.interimResults = false;
this.recognition.onresult = (event: any) => {
if (event.results && event.results[0]) {
const transcript = event.results[0][0].transcript.trim();
console.log('Recognized speech:', transcript);
this.userInput = transcript;
this.sendMessage();
this.recognition.stop();
this.isListening = false;
}
};
this.recognition.onerror = (event: any) => {
console.error('Speech Recognition Error:', event.error);
this.isProcessingSpeech = false;
};
} else {
console.warn('Speech Recognition is not supported in this browser.');
}
window.addEventListener('beforeunload', this.handleUnload);
}
}
private handleUnload = (): void => {
if (window.speechSynthesis) {
window.speechSynthesis.cancel();
}
};
ngOnDestroy(): void {
if (isPlatformBrowser(this.platformId)) {
if (window.speechSynthesis) {
window.speechSynthesis.cancel();
}
window.removeEventListener('beforeunload', this.handleUnload);
}
}
openMicrophonePopup(): void {
this.showMicPopup = true;
}
closeMicrophonePopup(): void {
this.showMicPopup = false;
}
showHardcodedQuestions(): void {
setTimeout(() => {
this.showQuestions = true;
}, 100);
}
hideHardcodedQuestions(): void {
setTimeout(() => {
this.showQuestions = false;
}, 200);
}
selectHardcodedQuestion(question: string): void {
this.userInput = question;
this.showQuestions = false;
setTimeout(() => {
this.sendMessage();
this.userInput = '';
}, 100);
}
getSuggestions(): void {
if (!this.userInput || this.userInput.trim().length < 1 || this.isSpeaking) {
this.suggestions = [];
return;
}
this.apiService.getGrammarSuggestions(this.userInput).subscribe(
(response) => {
console.log("API Response:", response);
if (response.suggestions) {
this.suggestions = response.suggestions
.filter((s: string) => s && s.trim().length > 0)
.map((s: string) => s.replace(/^\d+\.\s*/, ""));
} else {
this.suggestions = [];
}
},
(error) => {
console.error("Error fetching suggestions:", error);
this.suggestions = [];
}
);
}
selectSuggestion(suggestion: string): void {
this.userInput = suggestion;
this.suggestions = [];
this.sendMessage();
}
sendMessage(inputText?: string): void {
const message = inputText ? inputText.trim() : this.userInput.trim();
if (!message) {
return;
}
let sessionId = localStorage.getItem('session_id');
this.messages.push({ from: 'user', text: message, timestamp: new Date().toLocaleTimeString() });
this.userInput = '';
this.isTyping = true;
this.cdr.detectChanges();
this.scrollToBottom();
this.responseSub = this.apiService.askQuestion(message, sessionId).subscribe(
(response) => {
this.isTyping = false;
const explanation = (response?.response || 'No explanation available.').trim();
if (response.session_id && !sessionId) {
localStorage.setItem('session_id', response.session_id);
}
const lines: string[] = String(explanation).split('\n');
const formatted: string = lines.map((line: string) => line.trim()).join('\n');
this.messages.push({
from: 'ai',
text: formatted,
timestamp: new Date().toLocaleTimeString(),
});
this.cdr.detectChanges();
this.scrollToBottom();
this.lastFullAiText = formatted;
this.speakResponse(explanation);
},
(error) => {
this.isTyping = false;
const errorMessage = 'Error: Could not get a response from the server.';
console.error('API Error:', error);
this.messages.push({
from: 'ai',
text: errorMessage,
timestamp: new Date().toLocaleTimeString(),
});
this.cdr.detectChanges();
this.scrollToBottom();
this.lastFullAiText = errorMessage;
this.speakResponse(errorMessage);
}
);
}
formatStructuredResponse(text: string): string {
let formattedText = text
.replace(/\n/g, '
')
.replace(/(\d+)\.\s/g, '$1. ')
.replace(/\•\s/g, '✔️ ')
.replace(/\-\s/g, '🔹 ')
.replace(/(\*\*)(.*?)\1/g, '$2');
return formattedText;
}
speakResponse(responseText: string): void {
if (!responseText) {
console.warn('No response text provided for speech.');
return;
}
console.log('Initiating text-to-speech with response:', responseText);
let lastAiMessage = this.messages.slice().reverse().find((msg) => msg.from === 'ai');
if (!lastAiMessage) {
lastAiMessage = { from: 'ai', text: '', timestamp: new Date().toLocaleTimeString() };
this.messages.push(lastAiMessage);
} else {
lastAiMessage.text = '';
}
this.cdr.detectChanges();
const words = responseText.split(' ');
let currentWordIndex = 0;
const speech = new SpeechSynthesisUtterance();
speech.text = responseText;
speech.lang = 'en-US';
speech.pitch = 1;
speech.rate = 1;
this.isSpeaking = true;
const voices = window.speechSynthesis.getVoices();
let femaleVoice = voices.find(voice => voice.name === "Microsoft Zira - English (United States)");
if (femaleVoice) {
speech.voice = femaleVoice;
console.log("Using voice:", femaleVoice.name);
} else {
console.warn("Microsoft Zira not found, using default.");
}
speech.onboundary = (event) => {
if (event.name === 'word' && currentWordIndex < words.length) {
lastAiMessage!.text = words.slice(0, currentWordIndex + 1).join(' ');
currentWordIndex++;
this.cdr.detectChanges();
}
};
speech.onend = () => {
console.log('Speech ended.');
this.isSpeaking = false;
lastAiMessage!.text = responseText;
this.cdr.detectChanges();
};
console.log('Starting speech synthesis...');
window.speechSynthesis.speak(speech);
}
ngOnInit(): void {
if (window.speechSynthesis.onvoiceschanged !== undefined) {
window.speechSynthesis.onvoiceschanged = () => {
this.loadVoices();
};
}
this.loadVoices();
}
loadVoices(): void {
const voices = window.speechSynthesis.getVoices();
if (!voices.length) {
console.warn("No voices available yet, retrying...");
setTimeout(() => this.loadVoices(), 500);
return;
}
console.log("Available Voices:", voices.map(v => v.name));
const preferredVoices = [
"Google UK English Female",
"Google US English Female",
"Microsoft Zira - English (United States)",
"Microsoft Hazel - English (United Kingdom)",
"Google en-GB Female",
"Google en-US Female"
];
for (let voiceName of preferredVoices) {
const foundVoice = voices.find(voice => voice.name === voiceName);
if (foundVoice) {
this.selectedVoice = foundVoice;
break;
}
}
if (!this.selectedVoice) {
this.selectedVoice = voices.find(voice => voice.name.toLowerCase().includes("female")) || voices[0];
}
console.log("Selected AI Voice:", this.selectedVoice?.name);
}
pauseAudio(): void {
if (window.speechSynthesis.speaking && !window.speechSynthesis.paused) {
window.speechSynthesis.pause();
this.isAudioPaused = true;
console.log('AI Speech Paused');
this.cdr.detectChanges();
}
}
resumeAudio(): void {
if (window.speechSynthesis.paused) {
window.speechSynthesis.resume();
this.isAudioPaused = false;
console.log('AI Speech Resumed');
this.cdr.detectChanges();
}
}
muteMicrophone(): void {
console.log("Microphone muted");
}
startListening(): void {
this.isListening = true;
this.isProcessingSpeech = false;
if (navigator.mediaDevices && navigator.mediaDevices.getUserMedia) {
navigator.mediaDevices
.getUserMedia({ audio: true })
.then(() => {
if (this.recognition) {
console.log('Starting speech recognition...');
this.recognition.start();
this.recognition.onaudiostart = () => console.log('Audio capturing started.');
this.recognition.onspeechstart = () => console.log('Speech has been detected.');
this.recognition.onspeechend = () => console.log('Speech ended, processing...');
this.recognition.onaudioend = () => console.log('Audio capturing ended.');
this.recognition.onresult = (event: any) => {
if (event.results && event.results[0]) {
const transcript = event.results[0][0].transcript.trim();
console.log('Recognized speech:', transcript);
this.userInput = transcript;
if (this.userInput.trim()) {
console.log('Sending question automatically:', this.userInput);
this.sendMessage();
}
this.recognition.stop();
this.isListening = false;
}
};
this.recognition.onnomatch = () =>
alert('No speech detected. Please try again.');
this.recognition.onend = () => {
console.log('Speech recognition service disconnected.');
this.isListening = false;
};
this.recognition.onerror = (error: any) => {
console.error('Speech Recognition Error:', error);
this.isListening = false;
if (error.error === 'not-allowed') {
alert('Microphone permission denied.');
} else if (error.error === 'no-speech') {
alert('No speech detected. Please try speaking clearly.');
}
};
} else {
alert('Speech Recognition is not supported in this browser.');
}
})
.catch((error) => {
console.error('Microphone access denied:', error);
this.errorMessage = 'Please enable microphone access to use this feature.';
this.isListening = true;
});
} else {
alert('Microphone access is not supported in this browser.');
}
}
stopListening(): void {
this.isListening = false;
if (this.recognition) {
this.recognition.stop();
}
}
toggleAudio(message: { text: string, isPlaying?: boolean }): void {
if (this.speechSynthesisInstance && this.speechSynthesisInstance.text === message.text) {
if (message.isPlaying) {
window.speechSynthesis.pause();
message.isPlaying = false;
} else {
window.speechSynthesis.resume();
message.isPlaying = true;
}
} else {
if (this.speechSynthesisInstance) {
window.speechSynthesis.cancel();
}
this.messages.forEach((msg) => (msg.isPlaying = false));
message.isPlaying = true;
this.speechSynthesisInstance = new SpeechSynthesisUtterance(message.text);
this.speechSynthesisInstance.lang = 'en-US';
this.speechSynthesisInstance.pitch = 1;
this.speechSynthesisInstance.rate = 1;
this.speechSynthesisInstance.onend = () => {
message.isPlaying = false;
this.speechSynthesisInstance = null;
};
window.speechSynthesis.speak(this.speechSynthesisInstance);
}
}
goToHome() {
this.router.navigate(['/home']);
}
copySuccessIndex: number | null = null;
copyToClipboard(text: string, index: number): void {
navigator.clipboard.writeText(text).then(() => {
this.copySuccessIndex = index;
setTimeout(() => {
this.copySuccessIndex = null;
}, 2000);
}).catch(err => {
console.error('Failed to copy: ', err);
});
}
checkInput() {
this.isInputValid = this.userInput.trim().length > 0;
}
handleButtonClick(): void {
if (this.userInput.trim().length > 0) {
this.showQuestions = false;
const messageToSend = this.userInput;
this.userInput = '';
this.sendMessage(messageToSend);
} else if (this.isSpeaking) {
this.pauseAudio();
} else if (this.isAudioPaused) {
this.resumeAudio();
} else {
this.startListening();
}
}
getButtonIcon(): string {
if (this.userInput.trim().length > 0) {
return 'assets/images/chat/send-icon.png';
} else if (this.isSpeaking) {
return 'assets/images/chat/pause-icon.png';
} else if (this.isAudioPaused) {
return 'assets/images/chat/resume-icon.png';
} else {
return 'assets/images/chat/microphone-icon.png';
}
}
addNewLine(event: KeyboardEvent): void {
if (event.key === 'Enter' && event.shiftKey) {
event.preventDefault();
this.userInput += '\n';
}
}
adjustTextareaHeight(event: Event): void {
const textarea = event.target as HTMLTextAreaElement;
textarea.style.height = 'auto';
textarea.style.height = `${textarea.scrollHeight}px`;
}
getButtonIconClass(): string {
return this.userInput.trim().length > 0
? 'send-icon'
: this.isSpeaking
? 'pause-icon'
: this.isAudioPaused
? 'resume-icon'
: 'microphone-icon';
}
openMicrophoneSettings(): void {
const userAgent = navigator.userAgent;
if (userAgent.includes("Chrome")) {
window.open("chrome://settings/content/microphone", "_blank");
} else if (userAgent.includes("Firefox")) {
window.open("about:preferences#privacy", "_blank");
} else if (userAgent.includes("Edge")) {
window.open("edge://settings/content/microphone", "_blank");
} else {
alert("Please check your browser's settings to enable the microphone.");
}
}
stopSpeaking(): void {
try {
if (window.speechSynthesis.speaking || window.speechSynthesis.paused) {
window.speechSynthesis.cancel();
}
} catch { }
(this as any).speechSynthesisInstance = null;
if (this.responseSub && !this.responseSub.closed) {
this.responseSub.unsubscribe();
}
this.isSpeaking = false;
this.isAudioPaused = false;
this.isTyping = false;
}
handleEnterPress(event: KeyboardEvent): void {
if (this.isSpeaking) {
event.preventDefault();
return;
}
if (event.key === 'Enter' && !event.shiftKey) {
event.preventDefault();
const text = (this.userInput || '').trim();
if (text) this.sendMessage();
}
}
}