diff --git a/src/app/components/chat/chat.component.spec.ts b/src/app/components/chat/chat.component.spec.ts index 3770a126..a5ee5ddd 100644 --- a/src/app/components/chat/chat.component.spec.ts +++ b/src/app/components/chat/chat.component.spec.ts @@ -1985,6 +1985,17 @@ describe('ChatComponent', () => { }; expect((component as any).isEmptyMetadataEvent(event)).toBeFalse(); }); + + it('returns false when a marker event signals voiceActivity', () => { + const event = { + id: 'vad-1', + author: 'agent', + usageMetadata: {totalTokenCount: 1}, + voiceActivity: {voiceActivityType: 'ACTIVITY_START'}, + actions: {...EMPTY_ACTIONS}, + }; + expect((component as any).isEmptyMetadataEvent(event)).toBeFalse(); + }); }); describe('actionsAreEmpty', () => { diff --git a/src/app/components/chat/chat.component.ts b/src/app/components/chat/chat.component.ts index 327c2f64..7828f862 100644 --- a/src/app/components/chat/chat.component.ts +++ b/src/app/components/chat/chat.component.ts @@ -1407,6 +1407,7 @@ export class ChatComponent implements OnInit, AfterViewInit, OnDestroy { && apiEvent.output === undefined && apiEvent.inputTranscription === undefined && apiEvent.outputTranscription === undefined + && apiEvent.voiceActivity === undefined && !apiEvent.errorMessage && !apiEvent.errorCode && !apiEvent.systemInstructionChanged diff --git a/src/app/components/event-content/event-content.component.html b/src/app/components/event-content/event-content.component.html index a73cd619..cbf3f760 100644 --- a/src/app/components/event-content/event-content.component.html +++ b/src/app/components/event-content/event-content.component.html @@ -44,6 +44,22 @@ [tooltipTitle]="'Turn Complete'" > } + @if (uiEvent.event.voiceActivity?.voiceActivityType === 'ACTIVITY_START') { + + } + @if (uiEvent.event.voiceActivity?.voiceActivityType === 'ACTIVITY_END') { + + } @if (uiEvent.event.interrupted) { { + let component: EventContentComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + initTestBed(); + + await TestBed.configureTestingModule({ + imports: [EventContentComponent, NoopAnimationsModule], + providers: [ + { + provide: ChatPanelMessagesInjectionToken, + useValue: CHAT_PANEL_MESSAGES, + }, + { + provide: MatDialog, + useValue: {open: () => ({afterClosed: () => ({subscribe: () => {}})})}, + }, + ], + }).compileComponents(); + + fixture = TestBed.createComponent(EventContentComponent); + component = fixture.componentInstance; + }); + + describe('Voice Activity Events', () => { + it('renders Voice Activity Start chip with mic icon and tooltip', () => { + component.uiEvent = new UiEvent({ + role: 'bot', + event: { + id: 'vad-start-1', + voiceActivity: { + voiceActivityType: 'ACTIVITY_START', + audioOffset: '1.5s', + }, + } as any, + }); + component.index = 0; + fixture.detectChanges(); + + const buttons = fixture.debugElement.queryAll(By.css('app-hover-info-button')); + const startBtn = buttons.find(b => b.componentInstance.text === 'Voice Activity Start'); + expect(startBtn).toBeTruthy(); + expect(startBtn?.componentInstance.icon).toBe('mic'); + expect(startBtn?.componentInstance.tooltipContent).toBe('Started at 1.5s'); + }); + + it('renders Voice Activity End chip with mic_off icon and tooltip', () => { + component.uiEvent = new UiEvent({ + role: 'bot', + event: { + id: 'vad-end-1', + voiceActivity: { + voiceActivityType: 'ACTIVITY_END', + audioOffset: '3.2s', + }, + } as any, + }); + component.index = 1; + fixture.detectChanges(); + + const buttons = fixture.debugElement.queryAll(By.css('app-hover-info-button')); + const endBtn = buttons.find(b => b.componentInstance.text === 'Voice Activity End'); + expect(endBtn).toBeTruthy(); + expect(endBtn?.componentInstance.icon).toBe('mic_off'); + expect(endBtn?.componentInstance.tooltipContent).toBe('Ended at 3.2s'); + }); + + it('renders default tooltip when audio offset is missing', () => { + component.uiEvent = new UiEvent({ + role: 'bot', + event: { + id: 'vad-start-2', + voiceActivity: { + voiceActivityType: 'ACTIVITY_START', + }, + } as any, + }); + component.index = 0; + fixture.detectChanges(); + + const buttons = fixture.debugElement.queryAll(By.css('app-hover-info-button')); + const startBtn = buttons.find(b => b.componentInstance.text === 'Voice Activity Start'); + expect(startBtn?.componentInstance.tooltipContent).toBe('Started'); + }); + }); +}); diff --git a/src/app/core/models/types.ts b/src/app/core/models/types.ts index e5552fc2..b3771fd8 100644 --- a/src/app/core/models/types.ts +++ b/src/app/core/models/types.ts @@ -71,12 +71,18 @@ export declare interface LlmRequest { contents: GenAiContent[]; } +export declare interface VoiceActivity { + voiceActivityType?: string; + audioOffset?: string; +} + export declare interface LlmResponse { content: GenAiContent; error?: string; errorMessage?: string; errorCode?: string; longRunningToolIds?: string[]; + voiceActivity?: VoiceActivity; } export enum NodeStatus {