diff --git a/projects/voicewave-angular/src/lib/voicewave-angular.component.html b/projects/voicewave-angular/src/lib/voicewave-angular.component.html
index 39ae8ae..769d8ba 100644
--- a/projects/voicewave-angular/src/lib/voicewave-angular.component.html
+++ b/projects/voicewave-angular/src/lib/voicewave-angular.component.html
@@ -1,73 +1,22 @@
-
-
- @if(!restrictMode || hiddenActions) {
-
-
-
+
+
+
+
+
+
diff --git a/projects/voicewave-angular/src/lib/voicewave-angular.component.scss b/projects/voicewave-angular/src/lib/voicewave-angular.component.scss
index e69de29..986a9cc 100644
--- a/projects/voicewave-angular/src/lib/voicewave-angular.component.scss
+++ b/projects/voicewave-angular/src/lib/voicewave-angular.component.scss
@@ -0,0 +1,90 @@
+.voicewave {
+ align-items: center;
+ background-color: var(--main-bg-color-primary);
+ bottom: 0;
+ box-shadow: 0 4px 16px rgba(0,0,0,0.2);
+ display: flex;
+ font-family: arial,sans-serif;
+ font-size: 14px;
+ height: auto;
+ justify-content: center;
+ left: 0;
+ opacity: 0;
+ padding: 35px;
+ pointer-events: none;
+ position: absolute;
+ right: 0;
+ top: 0;
+ transition: .5s all;
+ user-select: none;
+ z-index: 99;
+ &.active {
+ opacity: 1;
+ pointer-events: all;
+ p {
+ opacity: 1;
+ transform: translateX(0);
+ }
+ .btn-voice { transform: scale(1); }
+ }
+ p {
+ font-size: 22px;
+ margin: 0;
+ opacity: 0;
+ padding: 0;
+ transform: translateX(100px);
+ transition: 1s all;
+ }
+ .exit {
+ &:before {
+ content: '';
+ display: block;
+ width: 100%;
+ height: 100%;
+ position: fixed;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ }
+ }
+}
+
+.btn-voice {
+ --mic-volume-size: 150px;
+ align-items: center;
+ background-color: rgba(var(--main-color-rgb), .05);
+ border-radius: 50%;
+ border: 0;
+ box-shadow: 0 0 0 1px rgba(#fff, .6), 0 10px 20px rgba(var(--main-color-rgb), .1);
+ cursor: pointer;
+ display: flex;
+ height: var(--mic-volume-size);
+ justify-content: center;
+ margin-left: 20px;
+ opacity: .5;
+ outline: none;
+ transition: .4s all;
+ transform: scale(0);
+ user-select: none;
+ width: var(--mic-volume-size);
+ min-width: var(--mic-volume-size);
+ &.active {
+ animation: 1s voiceAnimation infinite alternate;
+ opacity: 1;
+ svg { fill: #ea4335; }
+ }
+ &:hover {
+ transform: scale(1.1);
+ }
+ svg {
+ height: 50%;
+ width: 50%;
+ fill: var(--main-color)
+ }
+}
+
+@keyframes voiceAnimation {
+ 0% { box-shadow: 0 0 0 1px rgba(#fff, .6), 0 10px 20px rgba(var(--main-color-rgb), .1), 0 0 10px rgba(var(--main-color-rgb), .1); }
+ 100% { box-shadow: 0 0 0 1px rgba(#fff, .6), 0 10px 20px rgba(var(--main-color-rgb), .1), 0 0 0 30px rgba(var(--main-color-rgb), .1);}
+}
diff --git a/projects/voicewave-angular/src/lib/voicewave-angular.component.spec.ts b/projects/voicewave-angular/src/lib/voicewave-angular.component.spec.ts
index c03a7ca..93a325e 100644
--- a/projects/voicewave-angular/src/lib/voicewave-angular.component.spec.ts
+++ b/projects/voicewave-angular/src/lib/voicewave-angular.component.spec.ts
@@ -1,179 +1,24 @@
-import {
- ComponentFixture,
- TestBed,
- fakeAsync,
- tick,
-} from '@angular/core/testing';
-import { VoiceWaveAngularComponent } from '../voicewave-angular.component';
-import { By } from '@angular/platform-browser';
+import { ComponentFixture, TestBed } from '@angular/core/testing';
-describe('VoiceWaveAngularComponent', () => {
- let component: VoiceWaveAngularComponent;
- let fixture: ComponentFixture;
+import { VoiceWave } from './voicewave-angular.component';
+
+describe('VoiceWave', () => {
+ let component: VoiceWave;
+ let fixture: ComponentFixture;
beforeEach(async () => {
await TestBed.configureTestingModule({
- declarations: [VoiceWaveAngularComponent],
+ declarations: [VoiceWave],
}).compileComponents();
+ });
- fixture = TestBed.createComponent(VoiceWaveAngularComponent);
+ beforeEach(() => {
+ fixture = TestBed.createComponent(VoiceWave);
component = fixture.componentInstance;
fixture.detectChanges();
});
- it('should create the component', () => {
+ it('should create', () => {
expect(component).toBeTruthy();
});
-
- it('should render title and subtitle', () => {
- component.title = 'Test Title';
- component.subtitle = 'Test Subtitle';
- fixture.detectChanges();
-
- const titleElement = fixture.debugElement.query(
- By.css('.up-window-title')
- ).nativeElement;
- const subtitleElement = fixture.debugElement.query(
- By.css('.up-window-subtitle')
- ).nativeElement;
-
- expect(titleElement.textContent).toContain('Test Title');
- expect(subtitleElement.textContent).toContain('Test Subtitle');
- });
-
- it('should append modal to body when isOpen is true', fakeAsync(() => {
- component.isOpen.set(false);
- fixture.detectChanges();
- expect(document.body.querySelector('.up-window')).toBeNull();
-
- component.isOpen.set(true);
- fixture.detectChanges();
- tick(100);
- expect(document.body.querySelector('.up-window')).toBeTruthy();
-
- component.isOpen.set(false);
- fixture.detectChanges();
- tick(400);
- expect(document.body.querySelector('.up-window')).toBeNull();
- }));
-
- it('should not reappend modal if it is already in the body', fakeAsync(() => {
- component.isOpen.set(true);
- fixture.detectChanges();
- tick(600);
-
- spyOn(component, 'addModalToBody').and.callThrough();
-
- component.addModalToBody();
- fixture.detectChanges();
-
- expect(component.addModalToBody).toHaveBeenCalledTimes(1);
- expect(document.body.querySelectorAll('.up-window').length).toBe(1);
- }));
-
- it('should apply the correct animation class when opening and closing the window', fakeAsync(() => {
- component.animation = 'slide';
-
- component.isOpen.set(true);
- component.openingAnimation = true;
- component.closingAnimation = false;
- fixture.detectChanges();
-
- tick(100);
-
- let windowElement = fixture.debugElement.query(By.css('.up-window'));
-
- expect(windowElement.classes['slide']).toBeTrue();
-
- component.closingAnimation = true;
- component.openingAnimation = false;
- component.isOpen.set(false);
- fixture.detectChanges();
-
- tick(600);
-
- windowElement = fixture.debugElement.query(By.css('.up-window'));
-
- expect(windowElement.classes['slide-out']).toBeTrue();
- expect(component.isOpen()).toBeFalse();
- }));
-
- it('should trigger onConfirm and onCancel when buttons are clicked', async () => {
- spyOn(component, 'onConfirm').and.callThrough();
- spyOn(component, 'onCancel').and.callThrough();
-
- component.isOpen.set(true);
- fixture.detectChanges();
-
- await fixture.whenStable();
- fixture.detectChanges();
-
- const confirmButton = fixture.debugElement.query(By.css('.btn-confirm'));
- const cancelButton = fixture.debugElement.query(By.css('.btn-cancel'));
-
- expect(confirmButton).toBeTruthy();
- expect(cancelButton).toBeTruthy();
-
- confirmButton.nativeElement.click();
- await fixture.whenStable();
- expect(component.onConfirm).toHaveBeenCalled();
- expect(component.isOpen()).toBeFalse();
-
- component.isOpen.set(true);
- fixture.detectChanges();
-
- await fixture.whenStable();
- cancelButton.nativeElement.click();
- await fixture.whenStable();
- expect(component.onCancel).toHaveBeenCalled();
- expect(component.isOpen()).toBeFalse();
- });
-
- it('should apply custom class from @Input', () => {
- component.class = 'custom-class';
- fixture.detectChanges();
-
- const windowElement = fixture.debugElement.query(By.css('.up-window'));
- expect(windowElement.classes['custom-class']).toBeTruthy();
- });
-
- it('should set isOpen to true when openWindow is called', () => {
- component.isOpen.set(true);
- expect(component.isOpen()).toBeTrue();
- });
-
- it('should set isOpen to false after closeWindow is called', fakeAsync(() => {
- component.isOpen.set(true);
- fixture.detectChanges();
-
- component.isOpen.set(false);
- fixture.detectChanges();
-
- tick(600);
- expect(component.isOpen()).toBeFalse();
- }));
-
- it('should not show confirm and cancel buttons if hiddenActions is true', () => {
- component.hiddenActions = true;
- component.isOpen.set(true);
- fixture.detectChanges();
-
- const confirmButton = fixture.debugElement.query(By.css('.btn-confirm'));
- const cancelButton = fixture.debugElement.query(By.css('.btn-cancel'));
-
- expect(confirmButton).toBeNull();
- expect(cancelButton).toBeNull();
- });
-
- it('should show confirm and cancel buttons if hiddenActions is false', () => {
- component.hiddenActions = false;
- component.isOpen.set(true);
- fixture.detectChanges();
-
- const confirmButton = fixture.debugElement.query(By.css('.btn-confirm'));
- const cancelButton = fixture.debugElement.query(By.css('.btn-cancel'));
-
- expect(confirmButton).toBeTruthy();
- expect(cancelButton).toBeTruthy();
- });
});
diff --git a/projects/voicewave-angular/src/lib/voicewave-angular.component.ts b/projects/voicewave-angular/src/lib/voicewave-angular.component.ts
index bce325c..f0efcff 100644
--- a/projects/voicewave-angular/src/lib/voicewave-angular.component.ts
+++ b/projects/voicewave-angular/src/lib/voicewave-angular.component.ts
@@ -1,207 +1,129 @@
import {
Component,
- Input,
OnInit,
- ViewEncapsulation,
- signal,
- WritableSignal,
+ DoCheck,
+ Input,
Output,
EventEmitter,
- OnDestroy,
- ElementRef,
- ViewChild,
- effect,
ChangeDetectorRef,
} from '@angular/core';
@Component({
- selector: 'voicewave-angular',
- templateUrl: './voicewave-angular.component.html',
- styleUrls: [
- './voicewave-angular.variables.scss',
- './voicewave-angular.animation.scss',
- './voicewave-angular.component.scss',
- './voicewave-angular.btn.scss',
- ],
- encapsulation: ViewEncapsulation.None,
+ selector: 'voicewave',
+ templateUrl: './voice.component.html',
+ styleUrls: ['./voice.component.scss'],
})
-export class VoiceWaveAngularComponent implements OnInit, OnDestroy {
- @Input() title?: string;
- @Input() subtitle?: string;
- @Input() class: string | undefined;
- @Input() isOpen: WritableSignal = signal(false);
- @Input() drawer: 'bottom' | 'top' | 'left' | 'right' | '' = '';
- @Input() animation: string = this.drawer ? this.drawer : 'fade';
- @Input() restrictMode: boolean = false;
- @Input() fullScreen: boolean = false;
- @Input() blur: boolean = false;
- @Input() grayscale: boolean = false;
- @Input() hiddenActions: boolean = false;
- @Input() confirmText: string = 'Confirm';
- @Input() cancelText: string = 'Cancel';
- @Input() confirmType: string = 'primary';
- @Input() cancelType: string = 'default';
- @Input() buttonAlignment: 'start' | 'end' | 'center' = 'end';
- @Input() onConfirmClick: () => void = () => this.onConfirm();
- @Input() onCancelClick: () => void = () => this.onCancel();
-
- @Output() confirm = new EventEmitter();
- @Output() cancel = new EventEmitter();
-
- closingAnimation: boolean = false;
- openingAnimation: boolean = false;
- shakeAnimation: boolean = false;
- focusableElements!: NodeListOf;
- firstFocusableElement!: HTMLElement;
- lastFocusableElement!: HTMLElement;
-
- @ViewChild('modal') modal!: ElementRef;
-
- constructor(private cdr: ChangeDetectorRef) {
- effect(() => {
- if (this.isOpen()) {
- this.addModalToBody();
- this.startOpeningAnimation();
- document.body.classList.add('no-scroll');
- } else {
- this.startClosingAnimation();
- this.removeModalFromBody();
- document.body.classList.remove('no-scroll');
- }
- });
- }
+export class VoiceWave implements OnInit, DoCheck {
+ @Input() show: boolean = false;
+ @Output() updateVoice = new EventEmitter();
+ final_transcript: string = '';
+ recognizing: boolean = false;
+ ignore_onend: any;
+ recognition: any;
+ animationButton: boolean = false;
+ root = window.document;
+
+ constructor(private _cdr: ChangeDetectorRef) {}
ngOnInit(): void {
- document.addEventListener('keydown', this.handleKeydown.bind(this));
- }
-
- ngAfterViewInit(): void {
- if (this.modal) {
- document.body.appendChild(this.modal.nativeElement);
-
- this.focusableElements = this.modal.nativeElement.querySelectorAll(
- 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
- );
- if (this.focusableElements.length > 0) {
- this.firstFocusableElement = this.focusableElements[0];
- this.lastFocusableElement =
- this.focusableElements[this.focusableElements.length - 1];
- }
- }
- this.cdr.detectChanges();
+ this.voiceSetup();
+ this.show ? this.activeVoice() : this.desactiveVoice();
}
- addModalToBody() {
- if (this.modal && this.modal.nativeElement.parentNode !== document.body) {
- document.body.appendChild(this.modal.nativeElement);
- }
+ ngDoCheck(): void {
+ this.show ? this.activeVoice() : this.desactiveVoice();
}
- removeModalFromBody() {
- if (this.modal && this.modal.nativeElement.parentNode === document.body) {
- document.body.removeChild(this.modal.nativeElement);
- }
+ disableVoice() {
+ this.updateVoice.next(false);
}
- handleKeydown(event: KeyboardEvent) {
- if (event.key === 'Escape' && this.isOpen() && !this.restrictMode) {
- this.closeWindow();
- } else if (event.key === 'Escape' && this.isOpen() && this.restrictMode) {
- this.triggerShakeAnimation();
+ activeVoice() {
+ if (!this.recognizing) {
+ this.recognizing = true;
+ this.final_transcript = '';
+ this.recognition.start();
}
-
- if (event.key === 'Tab' && this.isOpen()) {
- this.trapFocus(event);
- }
- }
-
- triggerShakeAnimation() {
- this.shakeAnimation = true;
- setTimeout(() => {
- this.shakeAnimation = false;
- }, 300);
}
- trapFocus(event: KeyboardEvent) {
- const isShiftPressed = event.shiftKey;
- const activeElement = document.activeElement as HTMLElement;
-
- if (!isShiftPressed && activeElement === this.lastFocusableElement) {
- event.preventDefault();
- this.firstFocusableElement.focus();
- } else if (isShiftPressed && activeElement === this.firstFocusableElement) {
- event.preventDefault();
- this.lastFocusableElement.focus();
+ desactiveVoice() {
+ if (this.recognizing) {
+ this.recognizing = false;
+ this.animationButton = false;
+ this.recognition.stop();
}
}
- ngOnDestroy(): void {
- this.removeModalFromBody();
- document.removeEventListener('keydown', this.handleKeydown.bind(this));
- }
-
- hasFooterContent(): boolean {
- const footerContent = this.modal?.nativeElement.querySelector('[footer]');
- return !!footerContent && footerContent.childNodes.length > 0;
- }
-
- startOpeningAnimation() {
- this.openingAnimation = true;
- setTimeout(() => {
- this.openingAnimation = false;
- }, 400);
- }
-
- startClosingAnimation() {
- this.closingAnimation = true;
- setTimeout(() => {
- this.closingAnimation = false;
- }, 400);
- }
-
- closeWindow(from?: string) {
- if (from == 'overlay' && this.restrictMode) {
- this.triggerShakeAnimation();
+ voiceSetup() {
+ if (!('webkitSpeechRecognition' in window)) {
+ console.log('atualize SpeechRecognition');
} else {
- this.closingAnimation = true;
-
- setTimeout(() => {
- this.isOpen.set(false);
- this.closingAnimation = false;
- }, 400);
+ this.recognition = new (window).webkitSpeechRecognition();
+
+ this.recognition.continuous = false;
+ this.recognition.interimResults = true;
+
+ this.final_transcript = '';
+ this.ignore_onend = false;
+ this.root.querySelector('.voicewave p').textContent = 'Ative o microfone';
+
+ this.recognition.onstart = () => {
+ this.recognizing = true;
+ this.root.querySelector('.voicewave p').textContent = 'Fale agora';
+ this.animationButton = true;
+ console.log('onstart voice');
+ this._cdr.detectChanges();
+ };
+
+ this.recognition.onerror = (event: any) => {
+ this.animationButton = false;
+ if (event.error === 'no-speech') {
+ console.log('onerror voice no-speech');
+ this.ignore_onend = true;
+ }
+ if (event.error === 'audio-capture') {
+ console.log('onerror audio-capture');
+ this.ignore_onend = true;
+ }
+ if (event.error === 'not-allowed') {
+ this.root.querySelector('.voicewave p').textContent =
+ 'Ative o microfone';
+ this.ignore_onend = true;
+ }
+ this._cdr.detectChanges();
+ };
+
+ this.recognition.onend = () => {
+ this.recognizing = false;
+ if (this.ignore_onend) {
+ return;
+ }
+ if (!this.final_transcript) {
+ return;
+ }
+ this.root.querySelector('.voicewave .exit').click();
+ this.root.querySelector('.voicewave p').textContent = '';
+ };
+
+ this.recognition.onresult = (event: any) => {
+ var interim_transcript = '';
+ for (var i = event.resultIndex; i < event.results.length; ++i) {
+ if (event.results[i].isFinal) {
+ this.final_transcript += event.results[i][0].transcript;
+ } else {
+ interim_transcript += event.results[i][0].transcript;
+ }
+ }
+ if (interim_transcript) {
+ this.root.querySelector('.voicewave p').textContent =
+ interim_transcript;
+ }
+ if (this.final_transcript) {
+ this.root.querySelector('body').removeAttribute('style');
+ console.log(this.final_transcript);
+ this.recognition.stop();
+ }
+ };
}
}
-
- getClass() {
- return {
- ...(this.class ? { [this.class]: true } : {}),
- [this.animation]: this.openingAnimation && !this.closingAnimation,
- [`${this.animation}-out`]: this.closingAnimation,
- [`drawer drawer-${this.drawer}`]: !!this.drawer,
- shake: this.shakeAnimation,
- fullscreen: this.fullScreen,
- blur: this.blur,
- grayscale: this.grayscale,
- };
- }
-
- getButtonClass(type: string) {
- const buttonClasses: { [key: string]: string } = {
- primary: 'btn-primary',
- secondary: 'btn-secondary',
- danger: 'btn-danger',
- };
-
- return buttonClasses[type] || 'btn-default';
- }
-
- onCancel() {
- this.cancel.emit();
- this.closeWindow();
- }
-
- onConfirm() {
- this.confirm.emit();
- this.closeWindow();
- }
}