@hivegpt/hiveai-angular 0.0.598 → 0.0.600
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/esm2020/lib/components/voice-agent/components/voice-agent-modal/voice-agent-modal.component.mjs +41 -22
- package/esm2020/lib/components/voice-agent/services/audio-analyzer.service.mjs +18 -14
- package/esm2020/lib/components/voice-agent/services/voice-agent.service.mjs +29 -18
- package/fesm2015/hivegpt-hiveai-angular.mjs +85 -51
- package/fesm2015/hivegpt-hiveai-angular.mjs.map +1 -1
- package/fesm2020/hivegpt-hiveai-angular.mjs +85 -51
- package/fesm2020/hivegpt-hiveai-angular.mjs.map +1 -1
- package/lib/components/voice-agent/components/voice-agent-modal/voice-agent-modal.component.d.ts +10 -6
- package/lib/components/voice-agent/components/voice-agent-modal/voice-agent-modal.component.d.ts.map +1 -1
- package/lib/components/voice-agent/services/audio-analyzer.service.d.ts +2 -0
- package/lib/components/voice-agent/services/audio-analyzer.service.d.ts.map +1 -1
- package/lib/components/voice-agent/services/voice-agent.service.d.ts +3 -4
- package/lib/components/voice-agent/services/voice-agent.service.d.ts.map +1 -1
- package/package.json +1 -1
|
@@ -87,6 +87,7 @@ class AudioAnalyzerService {
|
|
|
87
87
|
this.ngZone = ngZone;
|
|
88
88
|
this.audioContext = null;
|
|
89
89
|
this.analyserNode = null;
|
|
90
|
+
this.sourceNode = null;
|
|
90
91
|
this.dataArray = null;
|
|
91
92
|
this.animationFrameId = null;
|
|
92
93
|
this.isRunning = false;
|
|
@@ -100,6 +101,7 @@ class AudioAnalyzerService {
|
|
|
100
101
|
this.WAVEFORM_BAR_COUNT = 60;
|
|
101
102
|
// Amplify raw amplitude so normal speech (±10–20 units) maps to visible levels
|
|
102
103
|
this.SENSITIVITY_MULTIPLIER = 5;
|
|
104
|
+
this.barsBuffer = new Array(this.WAVEFORM_BAR_COUNT).fill(0);
|
|
103
105
|
this.audioLevels$ = this.audioLevelsSubject.asObservable();
|
|
104
106
|
this.isUserSpeaking$ = this.isUserSpeakingSubject.asObservable();
|
|
105
107
|
}
|
|
@@ -112,13 +114,13 @@ class AudioAnalyzerService {
|
|
|
112
114
|
if (this.audioContext.state === 'suspended') {
|
|
113
115
|
this.audioContext.resume().catch(console.warn);
|
|
114
116
|
}
|
|
115
|
-
|
|
117
|
+
this.sourceNode = this.audioContext.createMediaStreamSource(stream);
|
|
116
118
|
this.analyserNode = this.audioContext.createAnalyser();
|
|
117
119
|
this.analyserNode.fftSize = 2048;
|
|
118
120
|
this.analyserNode.smoothingTimeConstant = 0.4;
|
|
119
121
|
const bufferLength = this.analyserNode.frequencyBinCount;
|
|
120
122
|
this.dataArray = new Uint8Array(bufferLength);
|
|
121
|
-
|
|
123
|
+
this.sourceNode.connect(this.analyserNode);
|
|
122
124
|
this.isRunning = true;
|
|
123
125
|
this.noiseFloorSamples = [];
|
|
124
126
|
this.noiseFloor = 0;
|
|
@@ -135,6 +137,10 @@ class AudioAnalyzerService {
|
|
|
135
137
|
cancelAnimationFrame(this.animationFrameId);
|
|
136
138
|
this.animationFrameId = null;
|
|
137
139
|
}
|
|
140
|
+
if (this.sourceNode) {
|
|
141
|
+
this.sourceNode.disconnect();
|
|
142
|
+
this.sourceNode = null;
|
|
143
|
+
}
|
|
138
144
|
if (this.analyserNode) {
|
|
139
145
|
this.analyserNode.disconnect();
|
|
140
146
|
this.analyserNode = null;
|
|
@@ -172,11 +178,12 @@ class AudioAnalyzerService {
|
|
|
172
178
|
const isSpeaking = rms > threshold;
|
|
173
179
|
// Generate waveform bars
|
|
174
180
|
const bars = this.generateWaveformBars(this.dataArray);
|
|
175
|
-
//
|
|
176
|
-
this.
|
|
177
|
-
this.isUserSpeakingSubject.next(isSpeaking);
|
|
178
|
-
|
|
179
|
-
|
|
181
|
+
// Only trigger change detection when speaking state actually changes
|
|
182
|
+
if (isSpeaking !== this.isUserSpeakingSubject.value) {
|
|
183
|
+
this.ngZone.run(() => this.isUserSpeakingSubject.next(isSpeaking));
|
|
184
|
+
}
|
|
185
|
+
// Emit audio levels outside Angular's zone — no CD triggered here
|
|
186
|
+
this.audioLevelsSubject.next(bars);
|
|
180
187
|
this.animationFrameId = requestAnimationFrame(() => this.analyze());
|
|
181
188
|
}
|
|
182
189
|
calculateRMS(data) {
|
|
@@ -188,16 +195,13 @@ class AudioAnalyzerService {
|
|
|
188
195
|
return Math.sqrt(sum / data.length);
|
|
189
196
|
}
|
|
190
197
|
generateWaveformBars(data) {
|
|
191
|
-
const bars = [];
|
|
192
198
|
const step = Math.floor(data.length / this.WAVEFORM_BAR_COUNT);
|
|
193
199
|
for (let i = 0; i < this.WAVEFORM_BAR_COUNT; i++) {
|
|
194
|
-
const
|
|
195
|
-
const value = data[index];
|
|
196
|
-
// Normalize and amplify so quiet speech produces visible movement
|
|
200
|
+
const value = data[i * step];
|
|
197
201
|
const normalized = Math.abs((value - 128) / 128) * 100 * this.SENSITIVITY_MULTIPLIER;
|
|
198
|
-
|
|
202
|
+
this.barsBuffer[i] = Math.min(100, Math.max(0, normalized));
|
|
199
203
|
}
|
|
200
|
-
return
|
|
204
|
+
return this.barsBuffer;
|
|
201
205
|
}
|
|
202
206
|
}
|
|
203
207
|
AudioAnalyzerService.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "15.2.10", ngImport: i0, type: AudioAnalyzerService, deps: [{ token: i0.NgZone }], target: i0.ɵɵFactoryTarget.Injectable });
|
|
@@ -413,7 +417,7 @@ class VoiceAgentService {
|
|
|
413
417
|
this.botAudioElement = null;
|
|
414
418
|
this.lifecycleToken = 0;
|
|
415
419
|
this.subscriptions = new Subscription();
|
|
416
|
-
this.
|
|
420
|
+
this.pcEventCleanup = [];
|
|
417
421
|
this.callState$ = this.callStateSubject.asObservable();
|
|
418
422
|
this.statusText$ = this.statusTextSubject.asObservable();
|
|
419
423
|
this.duration$ = this.durationSubject.asObservable();
|
|
@@ -422,12 +426,7 @@ class VoiceAgentService {
|
|
|
422
426
|
this.audioLevels$ = this.audioLevelsSubject.asObservable();
|
|
423
427
|
this.userTranscript$ = this.userTranscriptSubject.asObservable();
|
|
424
428
|
this.botTranscript$ = this.botTranscriptSubject.asObservable();
|
|
425
|
-
this.subscriptions.add(this.audioAnalyzer.audioLevels$.pipe(throttleTime(50)).subscribe((levels) => this.audioLevelsSubject.next(levels)));
|
|
426
|
-
}
|
|
427
|
-
ngOnDestroy() {
|
|
428
|
-
this.destroy$.next();
|
|
429
|
-
this.subscriptions.unsubscribe();
|
|
430
|
-
void this.disconnect();
|
|
429
|
+
this.subscriptions.add(this.audioAnalyzer.audioLevels$.pipe(throttleTime(50)).subscribe((levels) => this.ngZone.run(() => this.audioLevelsSubject.next(levels))));
|
|
431
430
|
}
|
|
432
431
|
/** Reset to idle (e.g. when modal re-opens so user can click Start Call). */
|
|
433
432
|
resetToIdle() {
|
|
@@ -514,32 +513,36 @@ class VoiceAgentService {
|
|
|
514
513
|
});
|
|
515
514
|
this.pcClient = pcClient;
|
|
516
515
|
// Bot audio arrives as a MediaStreamTrack — wire to a hidden <audio> element
|
|
517
|
-
|
|
516
|
+
const onTrackStarted = (track, participant) => {
|
|
518
517
|
if (!this.isLifecycleActive(connectToken))
|
|
519
518
|
return;
|
|
520
519
|
if (!participant?.local && track.kind === 'audio') {
|
|
521
520
|
this.ngZone.run(() => this.setupBotAudioTrack(track));
|
|
522
521
|
}
|
|
523
|
-
}
|
|
522
|
+
};
|
|
523
|
+
pcClient.on(RTVIEvent.TrackStarted, onTrackStarted);
|
|
524
524
|
// Speaking state comes straight from RTVI events
|
|
525
|
-
|
|
525
|
+
const onBotStarted = () => this.ngZone.run(() => {
|
|
526
526
|
if (!this.isLifecycleActive(connectToken))
|
|
527
527
|
return;
|
|
528
528
|
this.onBotStartedSpeaking();
|
|
529
|
-
})
|
|
530
|
-
pcClient.on(RTVIEvent.
|
|
529
|
+
});
|
|
530
|
+
pcClient.on(RTVIEvent.BotStartedSpeaking, onBotStarted);
|
|
531
|
+
const onBotStopped = () => this.ngZone.run(() => {
|
|
531
532
|
if (!this.isLifecycleActive(connectToken))
|
|
532
533
|
return;
|
|
533
534
|
this.onBotStoppedSpeaking();
|
|
534
|
-
})
|
|
535
|
-
pcClient.on(RTVIEvent.
|
|
535
|
+
});
|
|
536
|
+
pcClient.on(RTVIEvent.BotStoppedSpeaking, onBotStopped);
|
|
537
|
+
const onUserStarted = () => this.ngZone.run(() => {
|
|
536
538
|
if (!this.isLifecycleActive(connectToken))
|
|
537
539
|
return;
|
|
538
540
|
this.isUserSpeakingSubject.next(true);
|
|
539
541
|
this.callStateSubject.next('listening');
|
|
540
542
|
this.statusTextSubject.next('Listening...');
|
|
541
|
-
})
|
|
542
|
-
pcClient.on(RTVIEvent.
|
|
543
|
+
});
|
|
544
|
+
pcClient.on(RTVIEvent.UserStartedSpeaking, onUserStarted);
|
|
545
|
+
const onUserStopped = () => this.ngZone.run(() => {
|
|
543
546
|
if (!this.isLifecycleActive(connectToken))
|
|
544
547
|
return;
|
|
545
548
|
this.isUserSpeakingSubject.next(false);
|
|
@@ -548,7 +551,16 @@ class VoiceAgentService {
|
|
|
548
551
|
this.callStateSubject.next('connected');
|
|
549
552
|
this.statusTextSubject.next('Processing...');
|
|
550
553
|
}
|
|
551
|
-
})
|
|
554
|
+
});
|
|
555
|
+
pcClient.on(RTVIEvent.UserStoppedSpeaking, onUserStopped);
|
|
556
|
+
// Store cleanup fns so listeners are removed on disconnect
|
|
557
|
+
this.pcEventCleanup = [
|
|
558
|
+
() => pcClient.off(RTVIEvent.TrackStarted, onTrackStarted),
|
|
559
|
+
() => pcClient.off(RTVIEvent.BotStartedSpeaking, onBotStarted),
|
|
560
|
+
() => pcClient.off(RTVIEvent.BotStoppedSpeaking, onBotStopped),
|
|
561
|
+
() => pcClient.off(RTVIEvent.UserStartedSpeaking, onUserStarted),
|
|
562
|
+
() => pcClient.off(RTVIEvent.UserStoppedSpeaking, onUserStopped),
|
|
563
|
+
];
|
|
552
564
|
// Acquire mic (triggers browser permission prompt)
|
|
553
565
|
await pcClient.initDevices();
|
|
554
566
|
// Build headers using the browser Headers API (required by pipecat's APIRequest type)
|
|
@@ -676,6 +688,9 @@ class VoiceAgentService {
|
|
|
676
688
|
async cleanupPipecatClient() {
|
|
677
689
|
if (this.pcClient) {
|
|
678
690
|
try {
|
|
691
|
+
// Remove all event listeners before disconnecting
|
|
692
|
+
this.pcEventCleanup.forEach(fn => fn());
|
|
693
|
+
this.pcEventCleanup = [];
|
|
679
694
|
await this.pcClient.disconnect();
|
|
680
695
|
}
|
|
681
696
|
catch {
|
|
@@ -752,6 +767,9 @@ class VoiceAgentModalComponent {
|
|
|
752
767
|
this.isMicMuted = false;
|
|
753
768
|
this.isUserSpeaking = false;
|
|
754
769
|
this.audioLevels = [];
|
|
770
|
+
this.cdPending = false;
|
|
771
|
+
this.envelopeCache = [];
|
|
772
|
+
this.MAX_BAR_HEIGHT = 42;
|
|
755
773
|
this.subscriptions = [];
|
|
756
774
|
this.isConnecting = false;
|
|
757
775
|
}
|
|
@@ -782,28 +800,29 @@ class VoiceAgentModalComponent {
|
|
|
782
800
|
}
|
|
783
801
|
this.subscriptions.push(this.voiceAgentService.callState$.subscribe(state => {
|
|
784
802
|
this.callState = state;
|
|
785
|
-
this.
|
|
803
|
+
this.scheduleCD();
|
|
786
804
|
}));
|
|
787
805
|
this.subscriptions.push(this.voiceAgentService.statusText$.subscribe(text => {
|
|
788
806
|
this.statusText = text;
|
|
789
|
-
this.
|
|
807
|
+
this.scheduleCD();
|
|
790
808
|
}));
|
|
791
809
|
this.subscriptions.push(this.voiceAgentService.duration$.subscribe(duration => {
|
|
792
810
|
this.duration = duration;
|
|
793
|
-
this.
|
|
811
|
+
this.scheduleCD();
|
|
794
812
|
}));
|
|
795
813
|
this.subscriptions.push(this.voiceAgentService.isMicMuted$.subscribe(muted => {
|
|
796
814
|
this.isMicMuted = muted;
|
|
797
|
-
this.
|
|
815
|
+
this.scheduleCD();
|
|
798
816
|
}));
|
|
799
817
|
this.subscriptions.push(this.voiceAgentService.isUserSpeaking$.subscribe(speaking => {
|
|
800
818
|
this.isUserSpeaking = speaking;
|
|
801
|
-
this.
|
|
819
|
+
this.scheduleCD();
|
|
802
820
|
}));
|
|
803
821
|
this.subscriptions.push(this.voiceAgentService.audioLevels$.subscribe(levels => {
|
|
804
822
|
this.audioLevels = levels;
|
|
805
|
-
this.
|
|
823
|
+
this.scheduleCD();
|
|
806
824
|
}));
|
|
825
|
+
this.initEnvelopeCache(60);
|
|
807
826
|
// Always start from a fresh voice session to avoid stale/disconnected state flashes.
|
|
808
827
|
void this.openFreshCallSession();
|
|
809
828
|
}
|
|
@@ -811,6 +830,16 @@ class VoiceAgentModalComponent {
|
|
|
811
830
|
this.subscriptions.forEach(sub => sub.unsubscribe());
|
|
812
831
|
this.disconnect();
|
|
813
832
|
}
|
|
833
|
+
/** Batch multiple subscription emissions into a single CD cycle per frame. */
|
|
834
|
+
scheduleCD() {
|
|
835
|
+
if (!this.cdPending) {
|
|
836
|
+
this.cdPending = true;
|
|
837
|
+
requestAnimationFrame(() => {
|
|
838
|
+
this.cdPending = false;
|
|
839
|
+
this.cdr.markForCheck();
|
|
840
|
+
});
|
|
841
|
+
}
|
|
842
|
+
}
|
|
814
843
|
async startCall() {
|
|
815
844
|
if (this.isConnecting || (this.callState !== 'idle' && this.callState !== 'ended'))
|
|
816
845
|
return;
|
|
@@ -835,22 +864,27 @@ class VoiceAgentModalComponent {
|
|
|
835
864
|
toggleMic() {
|
|
836
865
|
this.voiceAgentService.toggleMic();
|
|
837
866
|
}
|
|
838
|
-
/**
|
|
839
|
-
|
|
840
|
-
* bell-curve envelope so centre bars are tallest and edge bars appear
|
|
841
|
-
* as tiny dots — matching the audio-waveform reference design.
|
|
842
|
-
*/
|
|
843
|
-
getWaveformHeight(level, index) {
|
|
844
|
-
const n = Math.min(100, Math.max(0, level ?? 0));
|
|
845
|
-
const total = this.audioLevels.length || 60;
|
|
867
|
+
/** Pre-compute gaussian envelope so the hot path is a single lookup + multiply. */
|
|
868
|
+
initEnvelopeCache(total) {
|
|
846
869
|
const center = (total - 1) / 2;
|
|
847
870
|
const sigma = total / 5;
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
871
|
+
this.envelopeCache = [];
|
|
872
|
+
for (let i = 0; i < total; i++) {
|
|
873
|
+
const envelope = Math.exp(-((i - center) ** 2) / (2 * sigma * sigma));
|
|
874
|
+
const minH = 2 + envelope * 3;
|
|
875
|
+
const maxH = 4 + envelope * 38;
|
|
876
|
+
this.envelopeCache.push({ min: minH, range: maxH - minH });
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
/** Returns a 0–1 scale factor for the waveform bar transform (GPU-composited). */
|
|
880
|
+
getWaveformScale(level, index) {
|
|
881
|
+
const cached = this.envelopeCache[index];
|
|
882
|
+
if (!cached)
|
|
883
|
+
return 0.05;
|
|
884
|
+
const n = Math.min(100, Math.max(0, level ?? 0));
|
|
885
|
+
return (cached.min + (n / 100) * cached.range) / this.MAX_BAR_HEIGHT;
|
|
853
886
|
}
|
|
887
|
+
trackByIndex(index) { return index; }
|
|
854
888
|
/** Status label for active call — driven by callState + service statusText. */
|
|
855
889
|
get statusLabel() {
|
|
856
890
|
switch (this.callState) {
|
|
@@ -881,10 +915,10 @@ class VoiceAgentModalComponent {
|
|
|
881
915
|
}
|
|
882
916
|
}
|
|
883
917
|
VoiceAgentModalComponent.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "15.2.10", ngImport: i0, type: VoiceAgentModalComponent, deps: [{ token: VoiceAgentService }, { token: AudioAnalyzerService }, { token: i0.Injector }, { token: i0.ChangeDetectorRef }], target: i0.ɵɵFactoryTarget.Component });
|
|
884
|
-
VoiceAgentModalComponent.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "15.2.10", type: VoiceAgentModalComponent, selector: "hivegpt-voice-agent-modal", inputs: { apiUrl: "apiUrl", token: "token", botId: "botId", conversationId: "conversationId", apiKey: "apiKey", eventToken: "eventToken", eventId: "eventId", eventUrl: "eventUrl", domainAuthority: "domainAuthority", agentName: "agentName", agentRole: "agentRole", agentAvatar: "agentAvatar", usersApiUrl: "usersApiUrl" }, outputs: { close: "close" }, ngImport: i0, template: "<div class=\"voice-agent-modal-overlay\" (click)=\"endCall()\">\n <div\n class=\"voice-container voice-agent-modal\"\n (click)=\"$event.stopPropagation()\"\n >\n <!-- Header -->\n <div class=\"header\">\n <div class=\"header-left\">\n <div class=\"header-icon\">\n <svg\n width=\"16\"\n height=\"16\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n xmlns=\"http://www.w3.org/2000/svg\"\n >\n <path\n d=\"M12 1C8.13 1 5 4.13 5 8V14C5 17.87 8.13 21 12 21C15.87 21 19 17.87 19 14V8C19 4.13 15.87 1 12 1Z\"\n fill=\"currentColor\"\n />\n <path\n d=\"M12 23C10.34 23 9 21.66 9 20H15C15 21.66 13.66 23 12 23Z\"\n fill=\"currentColor\"\n />\n </svg>\n </div>\n <span class=\"header-title\">Voice</span>\n </div>\n <button\n class=\"close-button\"\n (click)=\"endCall()\"\n type=\"button\"\n aria-label=\"Close\"\n >\n <svg\n width=\"24\"\n height=\"24\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n xmlns=\"http://www.w3.org/2000/svg\"\n >\n <path\n d=\"M18 6L6 18M6 6L18 18\"\n stroke=\"currentColor\"\n stroke-width=\"2\"\n stroke-linecap=\"round\"\n />\n </svg>\n </button>\n </div>\n\n <!-- Avatar Section with glow -->\n <div class=\"avatar-section\">\n <div class=\"avatar-glow\" [class.glow-talking]=\"isBotTalking\" [class.glow-listening]=\"callState === 'listening'\"></div>\n\n <!-- Particle ring \u2014 visible while bot is talking -->\n <div *ngIf=\"isBotTalking\" class=\"particles-container\">\n <span *ngFor=\"let i of [0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15]\"\n class=\"particle\"\n [style.--i]=\"i\"\n [style.animationDelay]=\"(i * 0.15) + 's'\">\n </span>\n </div>\n\n <div class=\"avatar-wrapper\" [class.speaking]=\"isBotTalking\" [class.listening]=\"callState === 'listening'\">\n <img class=\"avatar-image\" [src]=\"displayAvatarUrl\" alt=\"Nia\" />\n </div>\n </div>\n\n <!-- Agent Info: Nia + Collaboration Manager AI Agent Specialist -->\n <div class=\"agent-info\">\n <div class=\"agent-name\">\n Nia\n <span class=\"ai-badge\">AI</span>\n </div>\n <p class=\"agent-role\">COP30 AI Agent </p>\n </div>\n\n <!-- Start Call (when idle only) -->\n <div *ngIf=\"callState === 'idle'\" class=\"start-call-section\">\n <p *ngIf=\"statusText === 'Connection failed'\" class=\"error-message\">\n {{ statusText }}\n </p>\n <button\n class=\"start-call-button\"\n type=\"button\"\n [disabled]=\"isConnecting\"\n (click)=\"startCall()\"\n >\n <span *ngIf=\"isConnecting\">Connecting...</span>\n <span *ngIf=\"!isConnecting && statusText === 'Connection failed'\"\n >Retry</span\n >\n <span *ngIf=\"!isConnecting && statusText !== 'Connection failed'\"\n >Start Call</span\n >\n </button>\n </div>\n\n <!-- Call Ended: status + Call Again / Back to Chat -->\n <div *ngIf=\"callState === 'ended'\" class=\"call-ended-section\">\n <p class=\"call-ended-status\">\n <span class=\"status-text\">Call Ended</span>\n <span class=\"status-timer\">{{ duration }}</span>\n </p>\n <div class=\"call-ended-controls\">\n <button\n class=\"action-btn\"\n type=\"button\"\n (click)=\"callAgain()\"\n title=\"Call Again\"\n >\n <svg\n width=\"18\"\n height=\"18\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n stroke-width=\"2\"\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n xmlns=\"http://www.w3.org/2000/svg\"\n >\n <path d=\"M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8\" />\n <path d=\"M3 3v5h5\" />\n </svg>\n Call Again\n </button>\n <button\n class=\"action-btn\"\n type=\"button\"\n (click)=\"backToChat()\"\n title=\"Back to Chat\"\n >\n <svg\n width=\"18\"\n height=\"18\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n stroke-width=\"2\"\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n xmlns=\"http://www.w3.org/2000/svg\"\n >\n <path d=\"M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z\" />\n </svg>\n Back to Chat\n </button>\n </div>\n </div>\n\n <!-- Status (when connecting or in-call: Talking... / Listening / Connected + timer) -->\n <div\n class=\"status-indicator status-inline\"\n *ngIf=\"callState !== 'idle' && callState !== 'ended'\"\n >\n <div *ngIf=\"callState === 'connecting'\" class=\"status-connecting\">\n <svg\n class=\"spinner\"\n width=\"24\"\n height=\"24\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n xmlns=\"http://www.w3.org/2000/svg\"\n >\n <circle\n cx=\"12\"\n cy=\"12\"\n r=\"10\"\n stroke=\"currentColor\"\n stroke-width=\"2\"\n stroke-linecap=\"round\"\n stroke-dasharray=\"31.416\"\n stroke-dashoffset=\"31.416\"\n >\n <animate\n attributeName=\"stroke-dasharray\"\n dur=\"2s\"\n values=\"0 31.416;15.708 15.708;0 31.416;0 31.416\"\n repeatCount=\"indefinite\"\n />\n <animate\n attributeName=\"stroke-dashoffset\"\n dur=\"2s\"\n values=\"0;-15.708;-31.416;-31.416\"\n repeatCount=\"indefinite\"\n />\n </circle>\n </svg>\n <span class=\"status-text\">{{ statusText }}</span>\n </div>\n <div\n *ngIf=\"callState !== 'connecting'\"\n class=\"status-connected status-inline-row\"\n >\n <span class=\"status-text\" [class.status-talking]=\"isBotTalking\" [class.status-listening]=\"callState === 'listening'\" [class.status-processing]=\"isProcessing\">\n {{ statusLabel }}\n </span>\n\n <!-- Animated bars \u2014 visible while bot is talking -->\n <div *ngIf=\"isBotTalking\" class=\"voice-visualizer\">\n <div class=\"vbar\"></div>\n <div class=\"vbar\"></div>\n <div class=\"vbar\"></div>\n <div class=\"vbar\"></div>\n </div>\n\n <!-- Bouncing dots \u2014 visible during processing pause -->\n <div *ngIf=\"isProcessing\" class=\"processing-dots\">\n <span></span><span></span><span></span>\n </div>\n\n <span class=\"status-timer\">{{ duration }}</span>\n </div>\n </div>\n\n <!-- Waveform: always visible during an active call, active (coloured) when user speaks -->\n <div\n *ngIf=\"callState === 'connected' || callState === 'listening' || callState === 'talking'\"\n class=\"waveform-container\"\n >\n <div class=\"waveform-bars\">\n <div\n *ngFor=\"let level of audioLevels; let i = index\"\n class=\"waveform-bar\"\n [class.active]=\"isUserActive\"\n [style.height.px]=\"getWaveformHeight(level, i)\"\n ></div>\n </div>\n </div>\n\n <!-- Call Controls (when connected) -->\n <div\n class=\"controls\"\n *ngIf=\"\n callState === 'connecting' ||\n callState === 'connected' ||\n callState === 'listening' ||\n callState === 'talking'\n \"\n >\n <div\n style=\"\n display: flex;\n align-items: center;\n gap: 2px;\n flex-direction: column;\n \"\n >\n <button\n class=\"control-btn mic-btn\"\n [class.muted]=\"isMicMuted\"\n (click)=\"toggleMic()\"\n type=\"button\"\n [title]=\"isMicMuted ? 'Unmute' : 'Mute'\"\n >\n <!-- Microphone icon (unmuted) -->\n <svg\n *ngIf=\"!isMicMuted\"\n width=\"24\"\n height=\"24\"\n viewBox=\"-5 0 32 32\"\n xmlns=\"http://www.w3.org/2000/svg\"\n fill=\"currentColor\"\n >\n <g transform=\"translate(-105, -307)\">\n <path\n d=\"M111,314 C111,311.238 113.239,309 116,309 C118.761,309 121,311.238 121,314 L121,324 C121,326.762 118.761,329 116,329 C113.239,329 111,326.762 111,324 L111,314 L111,314 Z M116,331 C119.866,331 123,327.866 123,324 L123,314 C123,310.134 119.866,307 116,307 C112.134,307 109,310.134 109,314 L109,324 C109,327.866 112.134,331 116,331 L116,331 Z M127,326 L125,326 C124.089,330.007 120.282,333 116,333 C111.718,333 107.911,330.007 107,326 L105,326 C105.883,330.799 110.063,334.51 115,334.955 L115,337 L114,337 C113.448,337 113,337.448 113,338 C113,338.553 113.448,339 114,339 L118,339 C118.552,339 119,338.553 119,338 C119,337.448 118.552,337 118,337 L117,337 L117,334.955 C121.937,334.51 126.117,330.799 127,326 L127,326 Z\"\n />\n </g>\n </svg>\n <!-- Microphone icon (muted) -->\n <svg\n *ngIf=\"isMicMuted\"\n width=\"24\"\n height=\"24\"\n viewBox=\"-5 0 32 32\"\n xmlns=\"http://www.w3.org/2000/svg\"\n fill=\"currentColor\"\n >\n <g transform=\"translate(-105, -307)\">\n <path\n d=\"M111,314 C111,311.238 113.239,309 116,309 C118.761,309 121,311.238 121,314 L121,324 C121,326.762 118.761,329 116,329 C113.239,329 111,326.762 111,324 L111,314 L111,314 Z M116,331 C119.866,331 123,327.866 123,324 L123,314 C123,310.134 119.866,307 116,307 C112.134,307 109,310.134 109,314 L109,324 C109,327.866 112.134,331 116,331 L116,331 Z M127,326 L125,326 C124.089,330.007 120.282,333 116,333 C111.718,333 107.911,330.007 107,326 L105,326 C105.883,330.799 110.063,334.51 115,334.955 L115,337 L114,337 C113.448,337 113,337.448 113,338 C113,338.553 113.448,339 114,339 L118,339 C118.552,339 119,338.553 119,338 C119,337.448 118.552,337 118,337 L117,337 L117,334.955 C121.937,334.51 126.117,330.799 127,326 L127,326 Z\"\n />\n </g>\n <path\n d=\"M2 2 L30 30\"\n stroke=\"currentColor\"\n stroke-width=\"2\"\n stroke-linecap=\"round\"\n />\n </svg>\n </button>\n <span class=\"control-label\">Mute</span>\n </div>\n\n <div\n style=\"\n display: flex;\n align-items: center;\n gap: 2px;\n flex-direction: column;\n \"\n >\n <button\n class=\"control-btn end-call-btn\"\n (click)=\"hangUp()\"\n type=\"button\"\n title=\"End Call\"\n >\n <svg\n width=\"24\"\n height=\"24\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n xmlns=\"http://www.w3.org/2000/svg\"\n >\n <path\n d=\"M18 6L6 18M6 6L18 18\"\n stroke=\"currentColor\"\n stroke-width=\"2\"\n stroke-linecap=\"round\"\n />\n </svg>\n </button>\n <span class=\"control-label\">End Call</span>\n </div>\n </div>\n </div>\n</div>\n", styles: ["@charset \"UTF-8\";:host{display:block}.voice-agent-modal-overlay{position:fixed;inset:0;background:rgba(0,0,0,.5);display:flex;justify-content:flex-end;align-items:flex-end;z-index:99999;backdrop-filter:blur(4px);padding:24px;font-family:Segoe UI,Tahoma,Geneva,Verdana,sans-serif}.voice-container.voice-agent-modal{width:100%;max-width:440px;background:white;border-radius:30px;padding:30px;box-shadow:0 10px 40px #0000001a;text-align:center;position:relative;display:flex;flex-direction:column;align-items:center;min-height:600px;animation:modalEnter .3s ease-out}@keyframes modalEnter{0%{opacity:0;transform:translateY(20px)}to{opacity:1;transform:translateY(0)}}.header{width:100%;display:flex;align-items:center;justify-content:space-between;margin-bottom:5px}.header-left{display:flex;align-items:center;gap:8px}.header-icon{width:28px;height:28px;background:#0f172a;border-radius:50%;color:#fff;display:flex;align-items:center;justify-content:center}.header-title{font-size:18px;font-weight:500;color:#0f172a}.close-button{background:none;border:none;cursor:pointer;padding:8px;color:#0f172a;display:flex;align-items:center;justify-content:center;transition:color .2s}.close-button:hover{color:#475569}.avatar-section{position:relative;margin-bottom:24px}.avatar-wrapper{width:180px;height:180px;border-radius:50%;padding:6px;background:#0ea5a4;background:linear-gradient(135deg,#ccfbf1 0%,#0ea5a4 100%);display:flex;align-items:center;justify-content:center;position:relative}.avatar-image{width:100%;height:100%;border-radius:50%;object-fit:cover;border:4px solid white}.avatar-glow{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);width:240px;height:240px;background:radial-gradient(circle,rgba(14,165,164,.2) 0%,transparent 70%);z-index:-1;pointer-events:none;transition:opacity .4s ease}.avatar-glow.glow-talking{width:280px;height:280px;background:radial-gradient(circle,rgba(14,165,164,.35) 0%,transparent 65%);animation:glowPulse 1.5s ease-in-out infinite}.avatar-glow.glow-listening{background:radial-gradient(circle,rgba(99,102,241,.25) 0%,transparent 65%)}@keyframes glowPulse{0%,to{opacity:.7;transform:translate(-50%,-50%) scale(1)}50%{opacity:1;transform:translate(-50%,-50%) scale(1.08)}}.avatar-wrapper.speaking{animation:avatarPulse 1.4s ease-in-out infinite}.avatar-wrapper.listening{animation:avatarListenPulse 1.8s ease-in-out infinite}@keyframes avatarPulse{0%,to{box-shadow:0 0 #0ea5a480}50%{box-shadow:0 0 0 18px #0ea5a400}}@keyframes avatarListenPulse{0%,to{box-shadow:0 0 #6366f166}50%{box-shadow:0 0 0 14px #6366f100}}.particles-container{position:absolute;top:50%;left:50%;width:0;height:0;z-index:2;pointer-events:none}.particle{position:absolute;width:7px;height:7px;border-radius:50%;background:#0ea5a4;opacity:0;animation:particleOrbit 2.4s ease-in-out infinite;animation-delay:var(--delay, 0s);transform-origin:0 0}@keyframes particleOrbit{0%{opacity:0;transform:rotate(calc(var(--i, 0) * 22.5deg)) translateY(-108px) scale(.4)}25%{opacity:.9}75%{opacity:.9}to{opacity:0;transform:rotate(calc(var(--i, 0) * 22.5deg + 45deg)) translateY(-108px) scale(.4)}}.agent-info{margin-bottom:40px}.agent-name{font-size:24px;font-weight:700;color:#0f172a;margin-bottom:8px;display:flex;align-items:center;justify-content:center;gap:8px}.ai-badge{background:#0ea5a4;color:#fff;font-size:10px;font-weight:700;padding:2px 6px;border-radius:6px}.agent-role{font-size:16px;color:#0f172a;font-weight:500;margin:0}.start-call-section{display:flex;flex-direction:column;align-items:center;gap:16px;margin-bottom:24px}.error-message{color:#dc2626;font-size:14px;margin:0}.start-call-button{padding:14px 32px;font-size:16px;font-weight:600;color:#fff;background:#0ea5a4;border:none;border-radius:12px;cursor:pointer;transition:background .2s}.start-call-button:hover:not(:disabled){background:#0d9488}.start-call-button:disabled{opacity:.7;cursor:not-allowed!important}.status-indicator{display:flex;align-items:center;justify-content:center;gap:12px;margin-bottom:10px}.status-connecting{display:flex;align-items:center;gap:12px}.spinner{color:#0ea5a4;animation:spin 1s linear infinite}@keyframes spin{0%{transform:rotate(0)}to{transform:rotate(360deg)}}.status-text{font-size:16px;color:#0f172a;font-weight:400;transition:color .25s ease}.status-text.status-talking{color:#0ea5a4;font-weight:500}.status-text.status-listening{color:#2a2a2c;font-weight:500}.status-text.status-processing{color:#94a3b8}.status-timer{font-size:16px;color:#0f172a;font-weight:500}.voice-visualizer{display:flex;align-items:center;gap:3px;height:18px}.vbar{width:3px;height:6px;background:#0ea5a4;border-radius:2px;animation:vbarBounce 1s ease-in-out infinite}.vbar:nth-child(1){animation-delay:0s}.vbar:nth-child(2){animation-delay:.15s}.vbar:nth-child(3){animation-delay:.3s}.vbar:nth-child(4){animation-delay:.45s}@keyframes vbarBounce{0%,to{height:4px;opacity:.5}50%{height:16px;opacity:1}}.processing-dots{display:flex;align-items:center;gap:4px}.processing-dots span{display:inline-block;width:5px;height:5px;border-radius:50%;background:#94a3b8;animation:dotFade 1.2s ease-in-out infinite}.processing-dots span:nth-child(1){animation-delay:0s}.processing-dots span:nth-child(2){animation-delay:.2s}.processing-dots span:nth-child(3){animation-delay:.4s}@keyframes dotFade{0%,80%,to{transform:scale(.8);opacity:.4}40%{transform:scale(1.2);opacity:1}}.status-connected{display:flex;flex-direction:column;align-items:center;gap:4px}.status-inline .status-inline-row{flex-direction:row;align-items:center;gap:8px}.call-ended-section{display:flex;flex-direction:column;align-items:center;gap:16px;margin-bottom:24px}.call-ended-status{display:flex;align-items:center;justify-content:center;gap:8px;margin:0;font-size:16px;color:#0f172a}.call-ended-status .status-text{font-weight:400}.call-ended-status .status-timer{font-weight:500}.call-ended-controls{display:flex;justify-content:center;align-items:center;gap:16px;flex-wrap:wrap}.action-btn{display:flex;align-items:center;gap:8px;padding:12px 24px;border-radius:24px;font-size:14px;font-weight:500;color:#0f172a;background:white;border:1px solid #e2e8f0;cursor:pointer;transition:background .2s ease}.action-btn:hover{background:#f8fafc}.waveform-container{width:100%;display:flex;align-items:center;justify-content:center;height:56px;margin-bottom:10px;padding:0 8px}.waveform-bars{display:flex;align-items:center;justify-content:center;gap:2px;height:56px;width:100%}.waveform-bar{flex:0 0 2px;width:2px;min-height:2px;border-radius:99px;background:#cbd5e1;transition:height .1s ease-out}.waveform-bar.active{background:linear-gradient(180deg,#0ea5a4 0%,#0d9488 100%);box-shadow:0 0 4px #0ea5a480}.controls{display:flex;align-items:center;justify-content:center;gap:24px;width:100%}.control-btn{width:60px;height:60px;border-radius:50%;border:none;cursor:pointer;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:4px;transition:transform .2s ease}.control-btn:hover{transform:scale(1.05)}.control-btn:active{transform:scale(.95)}.control-label{font-size:12px;font-weight:500;color:#0f172a}.mic-btn{background:#e2e8f0;color:#475569}.mic-btn .control-label{color:#475569}.mic-btn.muted{background:#e2e8f0;color:#475569}.end-call-btn{background:#ef4444;color:#fff}.end-call-btn .control-label{color:#fff}.end-call-btn:hover{background:#dc2626}\n"], dependencies: [{ kind: "directive", type: i11.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i11.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
918
|
+
VoiceAgentModalComponent.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "15.2.10", type: VoiceAgentModalComponent, selector: "hivegpt-voice-agent-modal", inputs: { apiUrl: "apiUrl", token: "token", botId: "botId", conversationId: "conversationId", apiKey: "apiKey", eventToken: "eventToken", eventId: "eventId", eventUrl: "eventUrl", domainAuthority: "domainAuthority", agentName: "agentName", agentRole: "agentRole", agentAvatar: "agentAvatar", usersApiUrl: "usersApiUrl" }, outputs: { close: "close" }, ngImport: i0, template: "<div class=\"voice-agent-modal-overlay\" (click)=\"endCall()\">\n <div\n class=\"voice-container voice-agent-modal\"\n (click)=\"$event.stopPropagation()\"\n >\n <!-- Header -->\n <div class=\"header\">\n <div class=\"header-left\">\n <div class=\"header-icon\">\n <svg\n width=\"16\"\n height=\"16\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n xmlns=\"http://www.w3.org/2000/svg\"\n >\n <path\n d=\"M12 1C8.13 1 5 4.13 5 8V14C5 17.87 8.13 21 12 21C15.87 21 19 17.87 19 14V8C19 4.13 15.87 1 12 1Z\"\n fill=\"currentColor\"\n />\n <path\n d=\"M12 23C10.34 23 9 21.66 9 20H15C15 21.66 13.66 23 12 23Z\"\n fill=\"currentColor\"\n />\n </svg>\n </div>\n <span class=\"header-title\">Voice</span>\n </div>\n <button\n class=\"close-button\"\n (click)=\"endCall()\"\n type=\"button\"\n aria-label=\"Close\"\n >\n <svg\n width=\"24\"\n height=\"24\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n xmlns=\"http://www.w3.org/2000/svg\"\n >\n <path\n d=\"M18 6L6 18M6 6L18 18\"\n stroke=\"currentColor\"\n stroke-width=\"2\"\n stroke-linecap=\"round\"\n />\n </svg>\n </button>\n </div>\n\n <!-- Avatar Section with glow -->\n <div class=\"avatar-section\">\n <div class=\"avatar-glow\" [class.glow-talking]=\"isBotTalking\" [class.glow-listening]=\"callState === 'listening'\"></div>\n\n <!-- Particle ring \u2014 visible while bot is talking -->\n <div *ngIf=\"isBotTalking\" class=\"particles-container\">\n <span *ngFor=\"let i of [0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15]\"\n class=\"particle\"\n [style.--i]=\"i\"\n [style.animationDelay]=\"(i * 0.15) + 's'\">\n </span>\n </div>\n\n <div class=\"avatar-wrapper\" [class.speaking]=\"isBotTalking\" [class.listening]=\"callState === 'listening'\">\n <img class=\"avatar-image\" [src]=\"displayAvatarUrl\" alt=\"Nia\" />\n </div>\n </div>\n\n <!-- Agent Info: Nia + Collaboration Manager AI Agent Specialist -->\n <div class=\"agent-info\">\n <div class=\"agent-name\">\n Nia\n <span class=\"ai-badge\">AI</span>\n </div>\n <p class=\"agent-role\">COP30 AI Agent </p>\n </div>\n\n <!-- Start Call (when idle only) -->\n <div *ngIf=\"callState === 'idle'\" class=\"start-call-section\">\n <p *ngIf=\"statusText === 'Connection failed'\" class=\"error-message\">\n {{ statusText }}\n </p>\n <button\n class=\"start-call-button\"\n type=\"button\"\n [disabled]=\"isConnecting\"\n (click)=\"startCall()\"\n >\n <span *ngIf=\"isConnecting\">Connecting...</span>\n <span *ngIf=\"!isConnecting && statusText === 'Connection failed'\"\n >Retry</span\n >\n <span *ngIf=\"!isConnecting && statusText !== 'Connection failed'\"\n >Start Call</span\n >\n </button>\n </div>\n\n <!-- Call Ended: status + Call Again / Back to Chat -->\n <div *ngIf=\"callState === 'ended'\" class=\"call-ended-section\">\n <p class=\"call-ended-status\">\n <span class=\"status-text\">Call Ended</span>\n <span class=\"status-timer\">{{ duration }}</span>\n </p>\n <div class=\"call-ended-controls\">\n <button\n class=\"action-btn\"\n type=\"button\"\n (click)=\"callAgain()\"\n title=\"Call Again\"\n >\n <svg\n width=\"18\"\n height=\"18\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n stroke-width=\"2\"\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n xmlns=\"http://www.w3.org/2000/svg\"\n >\n <path d=\"M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8\" />\n <path d=\"M3 3v5h5\" />\n </svg>\n Call Again\n </button>\n <button\n class=\"action-btn\"\n type=\"button\"\n (click)=\"backToChat()\"\n title=\"Back to Chat\"\n >\n <svg\n width=\"18\"\n height=\"18\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n stroke-width=\"2\"\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n xmlns=\"http://www.w3.org/2000/svg\"\n >\n <path d=\"M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z\" />\n </svg>\n Back to Chat\n </button>\n </div>\n </div>\n\n <!-- Status (when connecting or in-call: Talking... / Listening / Connected + timer) -->\n <div\n class=\"status-indicator status-inline\"\n *ngIf=\"callState !== 'idle' && callState !== 'ended'\"\n >\n <div *ngIf=\"callState === 'connecting'\" class=\"status-connecting\">\n <svg\n class=\"spinner\"\n width=\"24\"\n height=\"24\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n xmlns=\"http://www.w3.org/2000/svg\"\n >\n <circle\n cx=\"12\"\n cy=\"12\"\n r=\"10\"\n stroke=\"currentColor\"\n stroke-width=\"2\"\n stroke-linecap=\"round\"\n stroke-dasharray=\"31.416\"\n stroke-dashoffset=\"31.416\"\n >\n <animate\n attributeName=\"stroke-dasharray\"\n dur=\"2s\"\n values=\"0 31.416;15.708 15.708;0 31.416;0 31.416\"\n repeatCount=\"indefinite\"\n />\n <animate\n attributeName=\"stroke-dashoffset\"\n dur=\"2s\"\n values=\"0;-15.708;-31.416;-31.416\"\n repeatCount=\"indefinite\"\n />\n </circle>\n </svg>\n <span class=\"status-text\">{{ statusText }}</span>\n </div>\n <div\n *ngIf=\"callState !== 'connecting'\"\n class=\"status-connected status-inline-row\"\n >\n <span class=\"status-text\" [class.status-talking]=\"isBotTalking\" [class.status-listening]=\"callState === 'listening'\" [class.status-processing]=\"isProcessing\">\n {{ statusLabel }}\n </span>\n\n <!-- Animated bars \u2014 visible while bot is talking -->\n <div *ngIf=\"isBotTalking\" class=\"voice-visualizer\">\n <div class=\"vbar\"></div>\n <div class=\"vbar\"></div>\n <div class=\"vbar\"></div>\n <div class=\"vbar\"></div>\n </div>\n\n <!-- Bouncing dots \u2014 visible during processing pause -->\n <div *ngIf=\"isProcessing\" class=\"processing-dots\">\n <span></span><span></span><span></span>\n </div>\n\n <span class=\"status-timer\">{{ duration }}</span>\n </div>\n </div>\n\n <!-- Waveform: space always reserved during active call, fades in when user speaks (unmuted) -->\n <div\n *ngIf=\"callState === 'connected' || callState === 'listening' || callState === 'talking'\"\n class=\"waveform-container\"\n [class.waveform-visible]=\"isUserActive\"\n >\n <div class=\"waveform-bars\">\n <div\n *ngFor=\"let level of audioLevels; let i = index; trackBy: trackByIndex\"\n class=\"waveform-bar\"\n [class.active]=\"isUserActive\"\n [style.transform]=\"'scaleY(' + getWaveformScale(level, i) + ')'\"\n ></div>\n </div>\n </div>\n\n <!-- Call Controls (when connected) -->\n <div\n class=\"controls\"\n *ngIf=\"\n callState === 'connecting' ||\n callState === 'connected' ||\n callState === 'listening' ||\n callState === 'talking'\n \"\n >\n <div\n style=\"\n display: flex;\n align-items: center;\n gap: 2px;\n flex-direction: column;\n \"\n >\n <button\n class=\"control-btn mic-btn\"\n [class.muted]=\"isMicMuted\"\n (click)=\"toggleMic()\"\n type=\"button\"\n [title]=\"isMicMuted ? 'Unmute' : 'Mute'\"\n >\n <!-- Microphone icon (unmuted) -->\n <svg\n *ngIf=\"!isMicMuted\"\n width=\"24\"\n height=\"24\"\n viewBox=\"-5 0 32 32\"\n xmlns=\"http://www.w3.org/2000/svg\"\n fill=\"currentColor\"\n >\n <g transform=\"translate(-105, -307)\">\n <path\n d=\"M111,314 C111,311.238 113.239,309 116,309 C118.761,309 121,311.238 121,314 L121,324 C121,326.762 118.761,329 116,329 C113.239,329 111,326.762 111,324 L111,314 L111,314 Z M116,331 C119.866,331 123,327.866 123,324 L123,314 C123,310.134 119.866,307 116,307 C112.134,307 109,310.134 109,314 L109,324 C109,327.866 112.134,331 116,331 L116,331 Z M127,326 L125,326 C124.089,330.007 120.282,333 116,333 C111.718,333 107.911,330.007 107,326 L105,326 C105.883,330.799 110.063,334.51 115,334.955 L115,337 L114,337 C113.448,337 113,337.448 113,338 C113,338.553 113.448,339 114,339 L118,339 C118.552,339 119,338.553 119,338 C119,337.448 118.552,337 118,337 L117,337 L117,334.955 C121.937,334.51 126.117,330.799 127,326 L127,326 Z\"\n />\n </g>\n </svg>\n <!-- Microphone icon (muted) -->\n <svg\n *ngIf=\"isMicMuted\"\n width=\"24\"\n height=\"24\"\n viewBox=\"-5 0 32 32\"\n xmlns=\"http://www.w3.org/2000/svg\"\n fill=\"currentColor\"\n >\n <g transform=\"translate(-105, -307)\">\n <path\n d=\"M111,314 C111,311.238 113.239,309 116,309 C118.761,309 121,311.238 121,314 L121,324 C121,326.762 118.761,329 116,329 C113.239,329 111,326.762 111,324 L111,314 L111,314 Z M116,331 C119.866,331 123,327.866 123,324 L123,314 C123,310.134 119.866,307 116,307 C112.134,307 109,310.134 109,314 L109,324 C109,327.866 112.134,331 116,331 L116,331 Z M127,326 L125,326 C124.089,330.007 120.282,333 116,333 C111.718,333 107.911,330.007 107,326 L105,326 C105.883,330.799 110.063,334.51 115,334.955 L115,337 L114,337 C113.448,337 113,337.448 113,338 C113,338.553 113.448,339 114,339 L118,339 C118.552,339 119,338.553 119,338 C119,337.448 118.552,337 118,337 L117,337 L117,334.955 C121.937,334.51 126.117,330.799 127,326 L127,326 Z\"\n />\n </g>\n <path\n d=\"M2 2 L30 30\"\n stroke=\"currentColor\"\n stroke-width=\"2\"\n stroke-linecap=\"round\"\n />\n </svg>\n </button>\n <span class=\"control-label\">Mute</span>\n </div>\n\n <div\n style=\"\n display: flex;\n align-items: center;\n gap: 2px;\n flex-direction: column;\n \"\n >\n <button\n class=\"control-btn end-call-btn\"\n (click)=\"hangUp()\"\n type=\"button\"\n title=\"End Call\"\n >\n <svg\n width=\"24\"\n height=\"24\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n xmlns=\"http://www.w3.org/2000/svg\"\n >\n <path\n d=\"M18 6L6 18M6 6L18 18\"\n stroke=\"currentColor\"\n stroke-width=\"2\"\n stroke-linecap=\"round\"\n />\n </svg>\n </button>\n <span class=\"control-label\">End Call</span>\n </div>\n </div>\n </div>\n</div>\n", styles: ["@charset \"UTF-8\";:host{display:block}.voice-agent-modal-overlay{position:fixed;inset:0;background:rgba(0,0,0,.5);display:flex;justify-content:flex-end;align-items:flex-end;z-index:99999;padding:24px;font-family:Segoe UI,Tahoma,Geneva,Verdana,sans-serif}.voice-container.voice-agent-modal{width:100%;max-width:440px;background:white;border-radius:30px;padding:30px;box-shadow:0 10px 40px #0000001a;text-align:center;position:relative;display:flex;flex-direction:column;align-items:center;min-height:600px;animation:modalEnter .3s ease-out}@keyframes modalEnter{0%{opacity:0;transform:translateY(20px)}to{opacity:1;transform:translateY(0)}}.header{width:100%;display:flex;align-items:center;justify-content:space-between;margin-bottom:5px}.header-left{display:flex;align-items:center;gap:8px}.header-icon{width:28px;height:28px;background:#0f172a;border-radius:50%;color:#fff;display:flex;align-items:center;justify-content:center}.header-title{font-size:18px;font-weight:500;color:#0f172a}.close-button{background:none;border:none;cursor:pointer;padding:8px;color:#0f172a;display:flex;align-items:center;justify-content:center;transition:color .2s}.close-button:hover{color:#475569}.avatar-section{position:relative;margin-bottom:24px}.avatar-wrapper{width:180px;height:180px;border-radius:50%;padding:6px;background:#0ea5a4;background:linear-gradient(135deg,#ccfbf1 0%,#0ea5a4 100%);display:flex;align-items:center;justify-content:center;position:relative}.avatar-image{width:100%;height:100%;border-radius:50%;object-fit:cover;border:4px solid white}.avatar-glow{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);width:240px;height:240px;background:radial-gradient(circle,rgba(14,165,164,.2) 0%,transparent 70%);z-index:-1;pointer-events:none;transition:opacity .4s ease}.avatar-glow.glow-talking{width:280px;height:280px;background:radial-gradient(circle,rgba(14,165,164,.35) 0%,transparent 65%);animation:glowPulse 1.5s ease-in-out infinite}.avatar-glow.glow-listening{background:radial-gradient(circle,rgba(99,102,241,.25) 0%,transparent 65%)}@keyframes glowPulse{0%,to{opacity:.7;transform:translate(-50%,-50%) scale(1)}50%{opacity:1;transform:translate(-50%,-50%) scale(1.08)}}.avatar-wrapper.speaking{animation:avatarPulse 1.4s ease-in-out infinite}.avatar-wrapper.listening{animation:avatarListenPulse 1.8s ease-in-out infinite}@keyframes avatarPulse{0%,to{box-shadow:0 0 #0ea5a480}50%{box-shadow:0 0 0 18px #0ea5a400}}@keyframes avatarListenPulse{0%,to{box-shadow:0 0 #6366f166}50%{box-shadow:0 0 0 14px #6366f100}}.particles-container{position:absolute;top:50%;left:50%;width:0;height:0;z-index:2;pointer-events:none}.particle{position:absolute;width:7px;height:7px;border-radius:50%;background:#0ea5a4;opacity:0;animation:particleOrbit 2.4s ease-in-out infinite;animation-delay:var(--delay, 0s);transform-origin:0 0}@keyframes particleOrbit{0%{opacity:0;transform:rotate(calc(var(--i, 0) * 22.5deg)) translateY(-108px) scale(.4)}25%{opacity:.9}75%{opacity:.9}to{opacity:0;transform:rotate(calc(var(--i, 0) * 22.5deg + 45deg)) translateY(-108px) scale(.4)}}.agent-info{margin-bottom:40px}.agent-name{font-size:24px;font-weight:700;color:#0f172a;margin-bottom:8px;display:flex;align-items:center;justify-content:center;gap:8px}.ai-badge{background:#0ea5a4;color:#fff;font-size:10px;font-weight:700;padding:2px 6px;border-radius:6px}.agent-role{font-size:16px;color:#0f172a;font-weight:500;margin:0}.start-call-section{display:flex;flex-direction:column;align-items:center;gap:16px;margin-bottom:24px}.error-message{color:#dc2626;font-size:14px;margin:0}.start-call-button{padding:14px 32px;font-size:16px;font-weight:600;color:#fff;background:#0ea5a4;border:none;border-radius:12px;cursor:pointer;transition:background .2s}.start-call-button:hover:not(:disabled){background:#0d9488}.start-call-button:disabled{opacity:.7;cursor:not-allowed!important}.status-indicator{display:flex;align-items:center;justify-content:center;gap:12px;margin-bottom:10px}.status-connecting{display:flex;align-items:center;gap:12px}.spinner{color:#0ea5a4;animation:spin 1s linear infinite}@keyframes spin{0%{transform:rotate(0)}to{transform:rotate(360deg)}}.status-text{font-size:16px;color:#0f172a;font-weight:400;transition:color .25s ease}.status-text.status-talking{color:#0ea5a4;font-weight:500}.status-text.status-listening{color:#2a2a2c;font-weight:500}.status-text.status-processing{color:#94a3b8}.status-timer{font-size:16px;color:#0f172a;font-weight:500}.voice-visualizer{display:flex;align-items:center;gap:3px;height:18px}.vbar{width:3px;height:6px;background:#0ea5a4;border-radius:2px;animation:vbarBounce 1s ease-in-out infinite}.vbar:nth-child(1){animation-delay:0s}.vbar:nth-child(2){animation-delay:.15s}.vbar:nth-child(3){animation-delay:.3s}.vbar:nth-child(4){animation-delay:.45s}@keyframes vbarBounce{0%,to{height:4px;opacity:.5}50%{height:16px;opacity:1}}.processing-dots{display:flex;align-items:center;gap:4px}.processing-dots span{display:inline-block;width:5px;height:5px;border-radius:50%;background:#94a3b8;animation:dotFade 1.2s ease-in-out infinite}.processing-dots span:nth-child(1){animation-delay:0s}.processing-dots span:nth-child(2){animation-delay:.2s}.processing-dots span:nth-child(3){animation-delay:.4s}@keyframes dotFade{0%,80%,to{transform:scale(.8);opacity:.4}40%{transform:scale(1.2);opacity:1}}.status-connected{display:flex;flex-direction:column;align-items:center;gap:4px}.status-inline .status-inline-row{flex-direction:row;align-items:center;gap:8px}.call-ended-section{display:flex;flex-direction:column;align-items:center;gap:16px;margin-bottom:24px}.call-ended-status{display:flex;align-items:center;justify-content:center;gap:8px;margin:0;font-size:16px;color:#0f172a}.call-ended-status .status-text{font-weight:400}.call-ended-status .status-timer{font-weight:500}.call-ended-controls{display:flex;justify-content:center;align-items:center;gap:16px;flex-wrap:wrap}.action-btn{display:flex;align-items:center;gap:8px;padding:12px 24px;border-radius:24px;font-size:14px;font-weight:500;color:#0f172a;background:white;border:1px solid #e2e8f0;cursor:pointer;transition:background .2s ease}.action-btn:hover{background:#f8fafc}.waveform-container{width:100%;display:flex;align-items:center;justify-content:center;height:56px;margin-bottom:10px;padding:0 8px;opacity:0;transition:opacity .3s ease}.waveform-container.waveform-visible{opacity:1}.waveform-bars{display:flex;align-items:center;justify-content:center;gap:2px;height:56px;width:100%;contain:layout style}.waveform-bar{flex:0 0 2px;width:2px;height:42px;border-radius:99px;background:#cbd5e1;transform-origin:center}.waveform-bar.active{background:linear-gradient(180deg,#0ea5a4 0%,#0d9488 100%);box-shadow:0 0 4px #0ea5a480}.controls{display:flex;align-items:center;justify-content:center;gap:24px;width:100%}.control-btn{width:60px;height:60px;border-radius:50%;border:none;cursor:pointer;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:4px;transition:transform .2s ease}.control-btn:hover{transform:scale(1.05)}.control-btn:active{transform:scale(.95)}.control-label{font-size:12px;font-weight:500;color:#0f172a}.mic-btn{background:#e2e8f0;color:#475569}.mic-btn .control-label{color:#475569}.mic-btn.muted{background:#e2e8f0;color:#475569}.end-call-btn{background:#ef4444;color:#fff}.end-call-btn .control-label{color:#fff}.end-call-btn:hover{background:#dc2626}\n"], dependencies: [{ kind: "directive", type: i11.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i11.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
885
919
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "15.2.10", ngImport: i0, type: VoiceAgentModalComponent, decorators: [{
|
|
886
920
|
type: Component,
|
|
887
|
-
args: [{ selector: 'hivegpt-voice-agent-modal', changeDetection: ChangeDetectionStrategy.OnPush, template: "<div class=\"voice-agent-modal-overlay\" (click)=\"endCall()\">\n <div\n class=\"voice-container voice-agent-modal\"\n (click)=\"$event.stopPropagation()\"\n >\n <!-- Header -->\n <div class=\"header\">\n <div class=\"header-left\">\n <div class=\"header-icon\">\n <svg\n width=\"16\"\n height=\"16\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n xmlns=\"http://www.w3.org/2000/svg\"\n >\n <path\n d=\"M12 1C8.13 1 5 4.13 5 8V14C5 17.87 8.13 21 12 21C15.87 21 19 17.87 19 14V8C19 4.13 15.87 1 12 1Z\"\n fill=\"currentColor\"\n />\n <path\n d=\"M12 23C10.34 23 9 21.66 9 20H15C15 21.66 13.66 23 12 23Z\"\n fill=\"currentColor\"\n />\n </svg>\n </div>\n <span class=\"header-title\">Voice</span>\n </div>\n <button\n class=\"close-button\"\n (click)=\"endCall()\"\n type=\"button\"\n aria-label=\"Close\"\n >\n <svg\n width=\"24\"\n height=\"24\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n xmlns=\"http://www.w3.org/2000/svg\"\n >\n <path\n d=\"M18 6L6 18M6 6L18 18\"\n stroke=\"currentColor\"\n stroke-width=\"2\"\n stroke-linecap=\"round\"\n />\n </svg>\n </button>\n </div>\n\n <!-- Avatar Section with glow -->\n <div class=\"avatar-section\">\n <div class=\"avatar-glow\" [class.glow-talking]=\"isBotTalking\" [class.glow-listening]=\"callState === 'listening'\"></div>\n\n <!-- Particle ring \u2014 visible while bot is talking -->\n <div *ngIf=\"isBotTalking\" class=\"particles-container\">\n <span *ngFor=\"let i of [0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15]\"\n class=\"particle\"\n [style.--i]=\"i\"\n [style.animationDelay]=\"(i * 0.15) + 's'\">\n </span>\n </div>\n\n <div class=\"avatar-wrapper\" [class.speaking]=\"isBotTalking\" [class.listening]=\"callState === 'listening'\">\n <img class=\"avatar-image\" [src]=\"displayAvatarUrl\" alt=\"Nia\" />\n </div>\n </div>\n\n <!-- Agent Info: Nia + Collaboration Manager AI Agent Specialist -->\n <div class=\"agent-info\">\n <div class=\"agent-name\">\n Nia\n <span class=\"ai-badge\">AI</span>\n </div>\n <p class=\"agent-role\">COP30 AI Agent </p>\n </div>\n\n <!-- Start Call (when idle only) -->\n <div *ngIf=\"callState === 'idle'\" class=\"start-call-section\">\n <p *ngIf=\"statusText === 'Connection failed'\" class=\"error-message\">\n {{ statusText }}\n </p>\n <button\n class=\"start-call-button\"\n type=\"button\"\n [disabled]=\"isConnecting\"\n (click)=\"startCall()\"\n >\n <span *ngIf=\"isConnecting\">Connecting...</span>\n <span *ngIf=\"!isConnecting && statusText === 'Connection failed'\"\n >Retry</span\n >\n <span *ngIf=\"!isConnecting && statusText !== 'Connection failed'\"\n >Start Call</span\n >\n </button>\n </div>\n\n <!-- Call Ended: status + Call Again / Back to Chat -->\n <div *ngIf=\"callState === 'ended'\" class=\"call-ended-section\">\n <p class=\"call-ended-status\">\n <span class=\"status-text\">Call Ended</span>\n <span class=\"status-timer\">{{ duration }}</span>\n </p>\n <div class=\"call-ended-controls\">\n <button\n class=\"action-btn\"\n type=\"button\"\n (click)=\"callAgain()\"\n title=\"Call Again\"\n >\n <svg\n width=\"18\"\n height=\"18\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n stroke-width=\"2\"\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n xmlns=\"http://www.w3.org/2000/svg\"\n >\n <path d=\"M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8\" />\n <path d=\"M3 3v5h5\" />\n </svg>\n Call Again\n </button>\n <button\n class=\"action-btn\"\n type=\"button\"\n (click)=\"backToChat()\"\n title=\"Back to Chat\"\n >\n <svg\n width=\"18\"\n height=\"18\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n stroke-width=\"2\"\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n xmlns=\"http://www.w3.org/2000/svg\"\n >\n <path d=\"M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z\" />\n </svg>\n Back to Chat\n </button>\n </div>\n </div>\n\n <!-- Status (when connecting or in-call: Talking... / Listening / Connected + timer) -->\n <div\n class=\"status-indicator status-inline\"\n *ngIf=\"callState !== 'idle' && callState !== 'ended'\"\n >\n <div *ngIf=\"callState === 'connecting'\" class=\"status-connecting\">\n <svg\n class=\"spinner\"\n width=\"24\"\n height=\"24\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n xmlns=\"http://www.w3.org/2000/svg\"\n >\n <circle\n cx=\"12\"\n cy=\"12\"\n r=\"10\"\n stroke=\"currentColor\"\n stroke-width=\"2\"\n stroke-linecap=\"round\"\n stroke-dasharray=\"31.416\"\n stroke-dashoffset=\"31.416\"\n >\n <animate\n attributeName=\"stroke-dasharray\"\n dur=\"2s\"\n values=\"0 31.416;15.708 15.708;0 31.416;0 31.416\"\n repeatCount=\"indefinite\"\n />\n <animate\n attributeName=\"stroke-dashoffset\"\n dur=\"2s\"\n values=\"0;-15.708;-31.416;-31.416\"\n repeatCount=\"indefinite\"\n />\n </circle>\n </svg>\n <span class=\"status-text\">{{ statusText }}</span>\n </div>\n <div\n *ngIf=\"callState !== 'connecting'\"\n class=\"status-connected status-inline-row\"\n >\n <span class=\"status-text\" [class.status-talking]=\"isBotTalking\" [class.status-listening]=\"callState === 'listening'\" [class.status-processing]=\"isProcessing\">\n {{ statusLabel }}\n </span>\n\n <!-- Animated bars \u2014 visible while bot is talking -->\n <div *ngIf=\"isBotTalking\" class=\"voice-visualizer\">\n <div class=\"vbar\"></div>\n <div class=\"vbar\"></div>\n <div class=\"vbar\"></div>\n <div class=\"vbar\"></div>\n </div>\n\n <!-- Bouncing dots \u2014 visible during processing pause -->\n <div *ngIf=\"isProcessing\" class=\"processing-dots\">\n <span></span><span></span><span></span>\n </div>\n\n <span class=\"status-timer\">{{ duration }}</span>\n </div>\n </div>\n\n <!-- Waveform: always visible during an active call, active (coloured) when user speaks -->\n <div\n *ngIf=\"callState === 'connected' || callState === 'listening' || callState === 'talking'\"\n class=\"waveform-container\"\n >\n <div class=\"waveform-bars\">\n <div\n *ngFor=\"let level of audioLevels; let i = index\"\n class=\"waveform-bar\"\n [class.active]=\"isUserActive\"\n [style.height.px]=\"getWaveformHeight(level, i)\"\n ></div>\n </div>\n </div>\n\n <!-- Call Controls (when connected) -->\n <div\n class=\"controls\"\n *ngIf=\"\n callState === 'connecting' ||\n callState === 'connected' ||\n callState === 'listening' ||\n callState === 'talking'\n \"\n >\n <div\n style=\"\n display: flex;\n align-items: center;\n gap: 2px;\n flex-direction: column;\n \"\n >\n <button\n class=\"control-btn mic-btn\"\n [class.muted]=\"isMicMuted\"\n (click)=\"toggleMic()\"\n type=\"button\"\n [title]=\"isMicMuted ? 'Unmute' : 'Mute'\"\n >\n <!-- Microphone icon (unmuted) -->\n <svg\n *ngIf=\"!isMicMuted\"\n width=\"24\"\n height=\"24\"\n viewBox=\"-5 0 32 32\"\n xmlns=\"http://www.w3.org/2000/svg\"\n fill=\"currentColor\"\n >\n <g transform=\"translate(-105, -307)\">\n <path\n d=\"M111,314 C111,311.238 113.239,309 116,309 C118.761,309 121,311.238 121,314 L121,324 C121,326.762 118.761,329 116,329 C113.239,329 111,326.762 111,324 L111,314 L111,314 Z M116,331 C119.866,331 123,327.866 123,324 L123,314 C123,310.134 119.866,307 116,307 C112.134,307 109,310.134 109,314 L109,324 C109,327.866 112.134,331 116,331 L116,331 Z M127,326 L125,326 C124.089,330.007 120.282,333 116,333 C111.718,333 107.911,330.007 107,326 L105,326 C105.883,330.799 110.063,334.51 115,334.955 L115,337 L114,337 C113.448,337 113,337.448 113,338 C113,338.553 113.448,339 114,339 L118,339 C118.552,339 119,338.553 119,338 C119,337.448 118.552,337 118,337 L117,337 L117,334.955 C121.937,334.51 126.117,330.799 127,326 L127,326 Z\"\n />\n </g>\n </svg>\n <!-- Microphone icon (muted) -->\n <svg\n *ngIf=\"isMicMuted\"\n width=\"24\"\n height=\"24\"\n viewBox=\"-5 0 32 32\"\n xmlns=\"http://www.w3.org/2000/svg\"\n fill=\"currentColor\"\n >\n <g transform=\"translate(-105, -307)\">\n <path\n d=\"M111,314 C111,311.238 113.239,309 116,309 C118.761,309 121,311.238 121,314 L121,324 C121,326.762 118.761,329 116,329 C113.239,329 111,326.762 111,324 L111,314 L111,314 Z M116,331 C119.866,331 123,327.866 123,324 L123,314 C123,310.134 119.866,307 116,307 C112.134,307 109,310.134 109,314 L109,324 C109,327.866 112.134,331 116,331 L116,331 Z M127,326 L125,326 C124.089,330.007 120.282,333 116,333 C111.718,333 107.911,330.007 107,326 L105,326 C105.883,330.799 110.063,334.51 115,334.955 L115,337 L114,337 C113.448,337 113,337.448 113,338 C113,338.553 113.448,339 114,339 L118,339 C118.552,339 119,338.553 119,338 C119,337.448 118.552,337 118,337 L117,337 L117,334.955 C121.937,334.51 126.117,330.799 127,326 L127,326 Z\"\n />\n </g>\n <path\n d=\"M2 2 L30 30\"\n stroke=\"currentColor\"\n stroke-width=\"2\"\n stroke-linecap=\"round\"\n />\n </svg>\n </button>\n <span class=\"control-label\">Mute</span>\n </div>\n\n <div\n style=\"\n display: flex;\n align-items: center;\n gap: 2px;\n flex-direction: column;\n \"\n >\n <button\n class=\"control-btn end-call-btn\"\n (click)=\"hangUp()\"\n type=\"button\"\n title=\"End Call\"\n >\n <svg\n width=\"24\"\n height=\"24\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n xmlns=\"http://www.w3.org/2000/svg\"\n >\n <path\n d=\"M18 6L6 18M6 6L18 18\"\n stroke=\"currentColor\"\n stroke-width=\"2\"\n stroke-linecap=\"round\"\n />\n </svg>\n </button>\n <span class=\"control-label\">End Call</span>\n </div>\n </div>\n </div>\n</div>\n", styles: ["@charset \"UTF-8\";:host{display:block}.voice-agent-modal-overlay{position:fixed;inset:0;background:rgba(0,0,0,.5);display:flex;justify-content:flex-end;align-items:flex-end;z-index:99999;backdrop-filter:blur(4px);padding:24px;font-family:Segoe UI,Tahoma,Geneva,Verdana,sans-serif}.voice-container.voice-agent-modal{width:100%;max-width:440px;background:white;border-radius:30px;padding:30px;box-shadow:0 10px 40px #0000001a;text-align:center;position:relative;display:flex;flex-direction:column;align-items:center;min-height:600px;animation:modalEnter .3s ease-out}@keyframes modalEnter{0%{opacity:0;transform:translateY(20px)}to{opacity:1;transform:translateY(0)}}.header{width:100%;display:flex;align-items:center;justify-content:space-between;margin-bottom:5px}.header-left{display:flex;align-items:center;gap:8px}.header-icon{width:28px;height:28px;background:#0f172a;border-radius:50%;color:#fff;display:flex;align-items:center;justify-content:center}.header-title{font-size:18px;font-weight:500;color:#0f172a}.close-button{background:none;border:none;cursor:pointer;padding:8px;color:#0f172a;display:flex;align-items:center;justify-content:center;transition:color .2s}.close-button:hover{color:#475569}.avatar-section{position:relative;margin-bottom:24px}.avatar-wrapper{width:180px;height:180px;border-radius:50%;padding:6px;background:#0ea5a4;background:linear-gradient(135deg,#ccfbf1 0%,#0ea5a4 100%);display:flex;align-items:center;justify-content:center;position:relative}.avatar-image{width:100%;height:100%;border-radius:50%;object-fit:cover;border:4px solid white}.avatar-glow{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);width:240px;height:240px;background:radial-gradient(circle,rgba(14,165,164,.2) 0%,transparent 70%);z-index:-1;pointer-events:none;transition:opacity .4s ease}.avatar-glow.glow-talking{width:280px;height:280px;background:radial-gradient(circle,rgba(14,165,164,.35) 0%,transparent 65%);animation:glowPulse 1.5s ease-in-out infinite}.avatar-glow.glow-listening{background:radial-gradient(circle,rgba(99,102,241,.25) 0%,transparent 65%)}@keyframes glowPulse{0%,to{opacity:.7;transform:translate(-50%,-50%) scale(1)}50%{opacity:1;transform:translate(-50%,-50%) scale(1.08)}}.avatar-wrapper.speaking{animation:avatarPulse 1.4s ease-in-out infinite}.avatar-wrapper.listening{animation:avatarListenPulse 1.8s ease-in-out infinite}@keyframes avatarPulse{0%,to{box-shadow:0 0 #0ea5a480}50%{box-shadow:0 0 0 18px #0ea5a400}}@keyframes avatarListenPulse{0%,to{box-shadow:0 0 #6366f166}50%{box-shadow:0 0 0 14px #6366f100}}.particles-container{position:absolute;top:50%;left:50%;width:0;height:0;z-index:2;pointer-events:none}.particle{position:absolute;width:7px;height:7px;border-radius:50%;background:#0ea5a4;opacity:0;animation:particleOrbit 2.4s ease-in-out infinite;animation-delay:var(--delay, 0s);transform-origin:0 0}@keyframes particleOrbit{0%{opacity:0;transform:rotate(calc(var(--i, 0) * 22.5deg)) translateY(-108px) scale(.4)}25%{opacity:.9}75%{opacity:.9}to{opacity:0;transform:rotate(calc(var(--i, 0) * 22.5deg + 45deg)) translateY(-108px) scale(.4)}}.agent-info{margin-bottom:40px}.agent-name{font-size:24px;font-weight:700;color:#0f172a;margin-bottom:8px;display:flex;align-items:center;justify-content:center;gap:8px}.ai-badge{background:#0ea5a4;color:#fff;font-size:10px;font-weight:700;padding:2px 6px;border-radius:6px}.agent-role{font-size:16px;color:#0f172a;font-weight:500;margin:0}.start-call-section{display:flex;flex-direction:column;align-items:center;gap:16px;margin-bottom:24px}.error-message{color:#dc2626;font-size:14px;margin:0}.start-call-button{padding:14px 32px;font-size:16px;font-weight:600;color:#fff;background:#0ea5a4;border:none;border-radius:12px;cursor:pointer;transition:background .2s}.start-call-button:hover:not(:disabled){background:#0d9488}.start-call-button:disabled{opacity:.7;cursor:not-allowed!important}.status-indicator{display:flex;align-items:center;justify-content:center;gap:12px;margin-bottom:10px}.status-connecting{display:flex;align-items:center;gap:12px}.spinner{color:#0ea5a4;animation:spin 1s linear infinite}@keyframes spin{0%{transform:rotate(0)}to{transform:rotate(360deg)}}.status-text{font-size:16px;color:#0f172a;font-weight:400;transition:color .25s ease}.status-text.status-talking{color:#0ea5a4;font-weight:500}.status-text.status-listening{color:#2a2a2c;font-weight:500}.status-text.status-processing{color:#94a3b8}.status-timer{font-size:16px;color:#0f172a;font-weight:500}.voice-visualizer{display:flex;align-items:center;gap:3px;height:18px}.vbar{width:3px;height:6px;background:#0ea5a4;border-radius:2px;animation:vbarBounce 1s ease-in-out infinite}.vbar:nth-child(1){animation-delay:0s}.vbar:nth-child(2){animation-delay:.15s}.vbar:nth-child(3){animation-delay:.3s}.vbar:nth-child(4){animation-delay:.45s}@keyframes vbarBounce{0%,to{height:4px;opacity:.5}50%{height:16px;opacity:1}}.processing-dots{display:flex;align-items:center;gap:4px}.processing-dots span{display:inline-block;width:5px;height:5px;border-radius:50%;background:#94a3b8;animation:dotFade 1.2s ease-in-out infinite}.processing-dots span:nth-child(1){animation-delay:0s}.processing-dots span:nth-child(2){animation-delay:.2s}.processing-dots span:nth-child(3){animation-delay:.4s}@keyframes dotFade{0%,80%,to{transform:scale(.8);opacity:.4}40%{transform:scale(1.2);opacity:1}}.status-connected{display:flex;flex-direction:column;align-items:center;gap:4px}.status-inline .status-inline-row{flex-direction:row;align-items:center;gap:8px}.call-ended-section{display:flex;flex-direction:column;align-items:center;gap:16px;margin-bottom:24px}.call-ended-status{display:flex;align-items:center;justify-content:center;gap:8px;margin:0;font-size:16px;color:#0f172a}.call-ended-status .status-text{font-weight:400}.call-ended-status .status-timer{font-weight:500}.call-ended-controls{display:flex;justify-content:center;align-items:center;gap:16px;flex-wrap:wrap}.action-btn{display:flex;align-items:center;gap:8px;padding:12px 24px;border-radius:24px;font-size:14px;font-weight:500;color:#0f172a;background:white;border:1px solid #e2e8f0;cursor:pointer;transition:background .2s ease}.action-btn:hover{background:#f8fafc}.waveform-container{width:100%;display:flex;align-items:center;justify-content:center;height:56px;margin-bottom:10px;padding:0 8px}.waveform-bars{display:flex;align-items:center;justify-content:center;gap:2px;height:56px;width:100%}.waveform-bar{flex:0 0 2px;width:2px;min-height:2px;border-radius:99px;background:#cbd5e1;transition:height .1s ease-out}.waveform-bar.active{background:linear-gradient(180deg,#0ea5a4 0%,#0d9488 100%);box-shadow:0 0 4px #0ea5a480}.controls{display:flex;align-items:center;justify-content:center;gap:24px;width:100%}.control-btn{width:60px;height:60px;border-radius:50%;border:none;cursor:pointer;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:4px;transition:transform .2s ease}.control-btn:hover{transform:scale(1.05)}.control-btn:active{transform:scale(.95)}.control-label{font-size:12px;font-weight:500;color:#0f172a}.mic-btn{background:#e2e8f0;color:#475569}.mic-btn .control-label{color:#475569}.mic-btn.muted{background:#e2e8f0;color:#475569}.end-call-btn{background:#ef4444;color:#fff}.end-call-btn .control-label{color:#fff}.end-call-btn:hover{background:#dc2626}\n"] }]
|
|
921
|
+
args: [{ selector: 'hivegpt-voice-agent-modal', changeDetection: ChangeDetectionStrategy.OnPush, template: "<div class=\"voice-agent-modal-overlay\" (click)=\"endCall()\">\n <div\n class=\"voice-container voice-agent-modal\"\n (click)=\"$event.stopPropagation()\"\n >\n <!-- Header -->\n <div class=\"header\">\n <div class=\"header-left\">\n <div class=\"header-icon\">\n <svg\n width=\"16\"\n height=\"16\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n xmlns=\"http://www.w3.org/2000/svg\"\n >\n <path\n d=\"M12 1C8.13 1 5 4.13 5 8V14C5 17.87 8.13 21 12 21C15.87 21 19 17.87 19 14V8C19 4.13 15.87 1 12 1Z\"\n fill=\"currentColor\"\n />\n <path\n d=\"M12 23C10.34 23 9 21.66 9 20H15C15 21.66 13.66 23 12 23Z\"\n fill=\"currentColor\"\n />\n </svg>\n </div>\n <span class=\"header-title\">Voice</span>\n </div>\n <button\n class=\"close-button\"\n (click)=\"endCall()\"\n type=\"button\"\n aria-label=\"Close\"\n >\n <svg\n width=\"24\"\n height=\"24\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n xmlns=\"http://www.w3.org/2000/svg\"\n >\n <path\n d=\"M18 6L6 18M6 6L18 18\"\n stroke=\"currentColor\"\n stroke-width=\"2\"\n stroke-linecap=\"round\"\n />\n </svg>\n </button>\n </div>\n\n <!-- Avatar Section with glow -->\n <div class=\"avatar-section\">\n <div class=\"avatar-glow\" [class.glow-talking]=\"isBotTalking\" [class.glow-listening]=\"callState === 'listening'\"></div>\n\n <!-- Particle ring \u2014 visible while bot is talking -->\n <div *ngIf=\"isBotTalking\" class=\"particles-container\">\n <span *ngFor=\"let i of [0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15]\"\n class=\"particle\"\n [style.--i]=\"i\"\n [style.animationDelay]=\"(i * 0.15) + 's'\">\n </span>\n </div>\n\n <div class=\"avatar-wrapper\" [class.speaking]=\"isBotTalking\" [class.listening]=\"callState === 'listening'\">\n <img class=\"avatar-image\" [src]=\"displayAvatarUrl\" alt=\"Nia\" />\n </div>\n </div>\n\n <!-- Agent Info: Nia + Collaboration Manager AI Agent Specialist -->\n <div class=\"agent-info\">\n <div class=\"agent-name\">\n Nia\n <span class=\"ai-badge\">AI</span>\n </div>\n <p class=\"agent-role\">COP30 AI Agent </p>\n </div>\n\n <!-- Start Call (when idle only) -->\n <div *ngIf=\"callState === 'idle'\" class=\"start-call-section\">\n <p *ngIf=\"statusText === 'Connection failed'\" class=\"error-message\">\n {{ statusText }}\n </p>\n <button\n class=\"start-call-button\"\n type=\"button\"\n [disabled]=\"isConnecting\"\n (click)=\"startCall()\"\n >\n <span *ngIf=\"isConnecting\">Connecting...</span>\n <span *ngIf=\"!isConnecting && statusText === 'Connection failed'\"\n >Retry</span\n >\n <span *ngIf=\"!isConnecting && statusText !== 'Connection failed'\"\n >Start Call</span\n >\n </button>\n </div>\n\n <!-- Call Ended: status + Call Again / Back to Chat -->\n <div *ngIf=\"callState === 'ended'\" class=\"call-ended-section\">\n <p class=\"call-ended-status\">\n <span class=\"status-text\">Call Ended</span>\n <span class=\"status-timer\">{{ duration }}</span>\n </p>\n <div class=\"call-ended-controls\">\n <button\n class=\"action-btn\"\n type=\"button\"\n (click)=\"callAgain()\"\n title=\"Call Again\"\n >\n <svg\n width=\"18\"\n height=\"18\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n stroke-width=\"2\"\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n xmlns=\"http://www.w3.org/2000/svg\"\n >\n <path d=\"M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8\" />\n <path d=\"M3 3v5h5\" />\n </svg>\n Call Again\n </button>\n <button\n class=\"action-btn\"\n type=\"button\"\n (click)=\"backToChat()\"\n title=\"Back to Chat\"\n >\n <svg\n width=\"18\"\n height=\"18\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n stroke-width=\"2\"\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n xmlns=\"http://www.w3.org/2000/svg\"\n >\n <path d=\"M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z\" />\n </svg>\n Back to Chat\n </button>\n </div>\n </div>\n\n <!-- Status (when connecting or in-call: Talking... / Listening / Connected + timer) -->\n <div\n class=\"status-indicator status-inline\"\n *ngIf=\"callState !== 'idle' && callState !== 'ended'\"\n >\n <div *ngIf=\"callState === 'connecting'\" class=\"status-connecting\">\n <svg\n class=\"spinner\"\n width=\"24\"\n height=\"24\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n xmlns=\"http://www.w3.org/2000/svg\"\n >\n <circle\n cx=\"12\"\n cy=\"12\"\n r=\"10\"\n stroke=\"currentColor\"\n stroke-width=\"2\"\n stroke-linecap=\"round\"\n stroke-dasharray=\"31.416\"\n stroke-dashoffset=\"31.416\"\n >\n <animate\n attributeName=\"stroke-dasharray\"\n dur=\"2s\"\n values=\"0 31.416;15.708 15.708;0 31.416;0 31.416\"\n repeatCount=\"indefinite\"\n />\n <animate\n attributeName=\"stroke-dashoffset\"\n dur=\"2s\"\n values=\"0;-15.708;-31.416;-31.416\"\n repeatCount=\"indefinite\"\n />\n </circle>\n </svg>\n <span class=\"status-text\">{{ statusText }}</span>\n </div>\n <div\n *ngIf=\"callState !== 'connecting'\"\n class=\"status-connected status-inline-row\"\n >\n <span class=\"status-text\" [class.status-talking]=\"isBotTalking\" [class.status-listening]=\"callState === 'listening'\" [class.status-processing]=\"isProcessing\">\n {{ statusLabel }}\n </span>\n\n <!-- Animated bars \u2014 visible while bot is talking -->\n <div *ngIf=\"isBotTalking\" class=\"voice-visualizer\">\n <div class=\"vbar\"></div>\n <div class=\"vbar\"></div>\n <div class=\"vbar\"></div>\n <div class=\"vbar\"></div>\n </div>\n\n <!-- Bouncing dots \u2014 visible during processing pause -->\n <div *ngIf=\"isProcessing\" class=\"processing-dots\">\n <span></span><span></span><span></span>\n </div>\n\n <span class=\"status-timer\">{{ duration }}</span>\n </div>\n </div>\n\n <!-- Waveform: space always reserved during active call, fades in when user speaks (unmuted) -->\n <div\n *ngIf=\"callState === 'connected' || callState === 'listening' || callState === 'talking'\"\n class=\"waveform-container\"\n [class.waveform-visible]=\"isUserActive\"\n >\n <div class=\"waveform-bars\">\n <div\n *ngFor=\"let level of audioLevels; let i = index; trackBy: trackByIndex\"\n class=\"waveform-bar\"\n [class.active]=\"isUserActive\"\n [style.transform]=\"'scaleY(' + getWaveformScale(level, i) + ')'\"\n ></div>\n </div>\n </div>\n\n <!-- Call Controls (when connected) -->\n <div\n class=\"controls\"\n *ngIf=\"\n callState === 'connecting' ||\n callState === 'connected' ||\n callState === 'listening' ||\n callState === 'talking'\n \"\n >\n <div\n style=\"\n display: flex;\n align-items: center;\n gap: 2px;\n flex-direction: column;\n \"\n >\n <button\n class=\"control-btn mic-btn\"\n [class.muted]=\"isMicMuted\"\n (click)=\"toggleMic()\"\n type=\"button\"\n [title]=\"isMicMuted ? 'Unmute' : 'Mute'\"\n >\n <!-- Microphone icon (unmuted) -->\n <svg\n *ngIf=\"!isMicMuted\"\n width=\"24\"\n height=\"24\"\n viewBox=\"-5 0 32 32\"\n xmlns=\"http://www.w3.org/2000/svg\"\n fill=\"currentColor\"\n >\n <g transform=\"translate(-105, -307)\">\n <path\n d=\"M111,314 C111,311.238 113.239,309 116,309 C118.761,309 121,311.238 121,314 L121,324 C121,326.762 118.761,329 116,329 C113.239,329 111,326.762 111,324 L111,314 L111,314 Z M116,331 C119.866,331 123,327.866 123,324 L123,314 C123,310.134 119.866,307 116,307 C112.134,307 109,310.134 109,314 L109,324 C109,327.866 112.134,331 116,331 L116,331 Z M127,326 L125,326 C124.089,330.007 120.282,333 116,333 C111.718,333 107.911,330.007 107,326 L105,326 C105.883,330.799 110.063,334.51 115,334.955 L115,337 L114,337 C113.448,337 113,337.448 113,338 C113,338.553 113.448,339 114,339 L118,339 C118.552,339 119,338.553 119,338 C119,337.448 118.552,337 118,337 L117,337 L117,334.955 C121.937,334.51 126.117,330.799 127,326 L127,326 Z\"\n />\n </g>\n </svg>\n <!-- Microphone icon (muted) -->\n <svg\n *ngIf=\"isMicMuted\"\n width=\"24\"\n height=\"24\"\n viewBox=\"-5 0 32 32\"\n xmlns=\"http://www.w3.org/2000/svg\"\n fill=\"currentColor\"\n >\n <g transform=\"translate(-105, -307)\">\n <path\n d=\"M111,314 C111,311.238 113.239,309 116,309 C118.761,309 121,311.238 121,314 L121,324 C121,326.762 118.761,329 116,329 C113.239,329 111,326.762 111,324 L111,314 L111,314 Z M116,331 C119.866,331 123,327.866 123,324 L123,314 C123,310.134 119.866,307 116,307 C112.134,307 109,310.134 109,314 L109,324 C109,327.866 112.134,331 116,331 L116,331 Z M127,326 L125,326 C124.089,330.007 120.282,333 116,333 C111.718,333 107.911,330.007 107,326 L105,326 C105.883,330.799 110.063,334.51 115,334.955 L115,337 L114,337 C113.448,337 113,337.448 113,338 C113,338.553 113.448,339 114,339 L118,339 C118.552,339 119,338.553 119,338 C119,337.448 118.552,337 118,337 L117,337 L117,334.955 C121.937,334.51 126.117,330.799 127,326 L127,326 Z\"\n />\n </g>\n <path\n d=\"M2 2 L30 30\"\n stroke=\"currentColor\"\n stroke-width=\"2\"\n stroke-linecap=\"round\"\n />\n </svg>\n </button>\n <span class=\"control-label\">Mute</span>\n </div>\n\n <div\n style=\"\n display: flex;\n align-items: center;\n gap: 2px;\n flex-direction: column;\n \"\n >\n <button\n class=\"control-btn end-call-btn\"\n (click)=\"hangUp()\"\n type=\"button\"\n title=\"End Call\"\n >\n <svg\n width=\"24\"\n height=\"24\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n xmlns=\"http://www.w3.org/2000/svg\"\n >\n <path\n d=\"M18 6L6 18M6 6L18 18\"\n stroke=\"currentColor\"\n stroke-width=\"2\"\n stroke-linecap=\"round\"\n />\n </svg>\n </button>\n <span class=\"control-label\">End Call</span>\n </div>\n </div>\n </div>\n</div>\n", styles: ["@charset \"UTF-8\";:host{display:block}.voice-agent-modal-overlay{position:fixed;inset:0;background:rgba(0,0,0,.5);display:flex;justify-content:flex-end;align-items:flex-end;z-index:99999;padding:24px;font-family:Segoe UI,Tahoma,Geneva,Verdana,sans-serif}.voice-container.voice-agent-modal{width:100%;max-width:440px;background:white;border-radius:30px;padding:30px;box-shadow:0 10px 40px #0000001a;text-align:center;position:relative;display:flex;flex-direction:column;align-items:center;min-height:600px;animation:modalEnter .3s ease-out}@keyframes modalEnter{0%{opacity:0;transform:translateY(20px)}to{opacity:1;transform:translateY(0)}}.header{width:100%;display:flex;align-items:center;justify-content:space-between;margin-bottom:5px}.header-left{display:flex;align-items:center;gap:8px}.header-icon{width:28px;height:28px;background:#0f172a;border-radius:50%;color:#fff;display:flex;align-items:center;justify-content:center}.header-title{font-size:18px;font-weight:500;color:#0f172a}.close-button{background:none;border:none;cursor:pointer;padding:8px;color:#0f172a;display:flex;align-items:center;justify-content:center;transition:color .2s}.close-button:hover{color:#475569}.avatar-section{position:relative;margin-bottom:24px}.avatar-wrapper{width:180px;height:180px;border-radius:50%;padding:6px;background:#0ea5a4;background:linear-gradient(135deg,#ccfbf1 0%,#0ea5a4 100%);display:flex;align-items:center;justify-content:center;position:relative}.avatar-image{width:100%;height:100%;border-radius:50%;object-fit:cover;border:4px solid white}.avatar-glow{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);width:240px;height:240px;background:radial-gradient(circle,rgba(14,165,164,.2) 0%,transparent 70%);z-index:-1;pointer-events:none;transition:opacity .4s ease}.avatar-glow.glow-talking{width:280px;height:280px;background:radial-gradient(circle,rgba(14,165,164,.35) 0%,transparent 65%);animation:glowPulse 1.5s ease-in-out infinite}.avatar-glow.glow-listening{background:radial-gradient(circle,rgba(99,102,241,.25) 0%,transparent 65%)}@keyframes glowPulse{0%,to{opacity:.7;transform:translate(-50%,-50%) scale(1)}50%{opacity:1;transform:translate(-50%,-50%) scale(1.08)}}.avatar-wrapper.speaking{animation:avatarPulse 1.4s ease-in-out infinite}.avatar-wrapper.listening{animation:avatarListenPulse 1.8s ease-in-out infinite}@keyframes avatarPulse{0%,to{box-shadow:0 0 #0ea5a480}50%{box-shadow:0 0 0 18px #0ea5a400}}@keyframes avatarListenPulse{0%,to{box-shadow:0 0 #6366f166}50%{box-shadow:0 0 0 14px #6366f100}}.particles-container{position:absolute;top:50%;left:50%;width:0;height:0;z-index:2;pointer-events:none}.particle{position:absolute;width:7px;height:7px;border-radius:50%;background:#0ea5a4;opacity:0;animation:particleOrbit 2.4s ease-in-out infinite;animation-delay:var(--delay, 0s);transform-origin:0 0}@keyframes particleOrbit{0%{opacity:0;transform:rotate(calc(var(--i, 0) * 22.5deg)) translateY(-108px) scale(.4)}25%{opacity:.9}75%{opacity:.9}to{opacity:0;transform:rotate(calc(var(--i, 0) * 22.5deg + 45deg)) translateY(-108px) scale(.4)}}.agent-info{margin-bottom:40px}.agent-name{font-size:24px;font-weight:700;color:#0f172a;margin-bottom:8px;display:flex;align-items:center;justify-content:center;gap:8px}.ai-badge{background:#0ea5a4;color:#fff;font-size:10px;font-weight:700;padding:2px 6px;border-radius:6px}.agent-role{font-size:16px;color:#0f172a;font-weight:500;margin:0}.start-call-section{display:flex;flex-direction:column;align-items:center;gap:16px;margin-bottom:24px}.error-message{color:#dc2626;font-size:14px;margin:0}.start-call-button{padding:14px 32px;font-size:16px;font-weight:600;color:#fff;background:#0ea5a4;border:none;border-radius:12px;cursor:pointer;transition:background .2s}.start-call-button:hover:not(:disabled){background:#0d9488}.start-call-button:disabled{opacity:.7;cursor:not-allowed!important}.status-indicator{display:flex;align-items:center;justify-content:center;gap:12px;margin-bottom:10px}.status-connecting{display:flex;align-items:center;gap:12px}.spinner{color:#0ea5a4;animation:spin 1s linear infinite}@keyframes spin{0%{transform:rotate(0)}to{transform:rotate(360deg)}}.status-text{font-size:16px;color:#0f172a;font-weight:400;transition:color .25s ease}.status-text.status-talking{color:#0ea5a4;font-weight:500}.status-text.status-listening{color:#2a2a2c;font-weight:500}.status-text.status-processing{color:#94a3b8}.status-timer{font-size:16px;color:#0f172a;font-weight:500}.voice-visualizer{display:flex;align-items:center;gap:3px;height:18px}.vbar{width:3px;height:6px;background:#0ea5a4;border-radius:2px;animation:vbarBounce 1s ease-in-out infinite}.vbar:nth-child(1){animation-delay:0s}.vbar:nth-child(2){animation-delay:.15s}.vbar:nth-child(3){animation-delay:.3s}.vbar:nth-child(4){animation-delay:.45s}@keyframes vbarBounce{0%,to{height:4px;opacity:.5}50%{height:16px;opacity:1}}.processing-dots{display:flex;align-items:center;gap:4px}.processing-dots span{display:inline-block;width:5px;height:5px;border-radius:50%;background:#94a3b8;animation:dotFade 1.2s ease-in-out infinite}.processing-dots span:nth-child(1){animation-delay:0s}.processing-dots span:nth-child(2){animation-delay:.2s}.processing-dots span:nth-child(3){animation-delay:.4s}@keyframes dotFade{0%,80%,to{transform:scale(.8);opacity:.4}40%{transform:scale(1.2);opacity:1}}.status-connected{display:flex;flex-direction:column;align-items:center;gap:4px}.status-inline .status-inline-row{flex-direction:row;align-items:center;gap:8px}.call-ended-section{display:flex;flex-direction:column;align-items:center;gap:16px;margin-bottom:24px}.call-ended-status{display:flex;align-items:center;justify-content:center;gap:8px;margin:0;font-size:16px;color:#0f172a}.call-ended-status .status-text{font-weight:400}.call-ended-status .status-timer{font-weight:500}.call-ended-controls{display:flex;justify-content:center;align-items:center;gap:16px;flex-wrap:wrap}.action-btn{display:flex;align-items:center;gap:8px;padding:12px 24px;border-radius:24px;font-size:14px;font-weight:500;color:#0f172a;background:white;border:1px solid #e2e8f0;cursor:pointer;transition:background .2s ease}.action-btn:hover{background:#f8fafc}.waveform-container{width:100%;display:flex;align-items:center;justify-content:center;height:56px;margin-bottom:10px;padding:0 8px;opacity:0;transition:opacity .3s ease}.waveform-container.waveform-visible{opacity:1}.waveform-bars{display:flex;align-items:center;justify-content:center;gap:2px;height:56px;width:100%;contain:layout style}.waveform-bar{flex:0 0 2px;width:2px;height:42px;border-radius:99px;background:#cbd5e1;transform-origin:center}.waveform-bar.active{background:linear-gradient(180deg,#0ea5a4 0%,#0d9488 100%);box-shadow:0 0 4px #0ea5a480}.controls{display:flex;align-items:center;justify-content:center;gap:24px;width:100%}.control-btn{width:60px;height:60px;border-radius:50%;border:none;cursor:pointer;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:4px;transition:transform .2s ease}.control-btn:hover{transform:scale(1.05)}.control-btn:active{transform:scale(.95)}.control-label{font-size:12px;font-weight:500;color:#0f172a}.mic-btn{background:#e2e8f0;color:#475569}.mic-btn .control-label{color:#475569}.mic-btn.muted{background:#e2e8f0;color:#475569}.end-call-btn{background:#ef4444;color:#fff}.end-call-btn .control-label{color:#fff}.end-call-btn:hover{background:#dc2626}\n"] }]
|
|
888
922
|
}], ctorParameters: function () { return [{ type: VoiceAgentService }, { type: AudioAnalyzerService }, { type: i0.Injector }, { type: i0.ChangeDetectorRef }]; }, propDecorators: { close: [{
|
|
889
923
|
type: Output
|
|
890
924
|
}], apiUrl: [{
|