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 {