Skip to content

Commit 49e810f

Browse files
hoonjicopybara-github
authored andcommitted
Adds optional feedback buttons to agent responses
Adds thumbs up/down buttons below the last response in a sequence of agent responses. This can allow sending requests to the planned FeedbackService (still under discussion for 3p usage). The feature is hidden behind a feature flag, which defaults to hidden. PiperOrigin-RevId: 837706895
1 parent b5db41c commit 49e810f

File tree

9 files changed

+228
-43
lines changed

9 files changed

+228
-43
lines changed

src/app/components/chat-panel/chat-panel.component.html

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
<div #autoScroll class="chat-messages">
2020
<div #videoContainer></div>
2121
@for (message of messages; track message; let i = $index) {
22+
<div class="message-column-container">
2223
<div
2324
[ngClass]="{
2425
'user-message': message.role === 'user',
@@ -288,6 +289,18 @@
288289
</button>
289290
}
290291
</div>
292+
<!-- Feedback UI -->
293+
@if(isUserFeedbackEnabled() && !isLoadingAgentResponse() && message.role === "bot") {
294+
<div class="feedback-buttons">
295+
<button mat-icon-button [matTooltip]="i18n.goodResponseTooltip" (click)="emitFeedback('up')">
296+
<mat-icon>thumb_up</mat-icon>
297+
</button>
298+
<button mat-icon-button [matTooltip]="i18n.badResponseTooltip" (click)="emitFeedback('down')">
299+
<mat-icon>thumb_down</mat-icon>
300+
</button>
301+
</div>
302+
}
303+
</div> <!-- end .message-column-container -->
291304
}
292305
</div>
293306
} @if (appName != "" && isChatMode && !isSessionLoading) {

src/app/components/chat-panel/chat-panel.component.i18n.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,8 @@ export const CHAT_PANEL_MESSAGES = {
4747
turnOffCamTooltip: 'Turn off camera',
4848
useCamTooltip: 'Use camera',
4949
updatedSessionStateChipLabel: 'Updated session state',
50+
goodResponseTooltip: 'Good response',
51+
badResponseTooltip: 'Bad response',
5052
};
5153

5254

src/app/components/chat-panel/chat-panel.component.scss

Lines changed: 49 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,9 @@
4545
position: relative;
4646
display: inline-block;
4747
&.message-card--highlighted {
48-
background-color: var(--chat-panel-function-event-button-highlight-background-color);
48+
background-color: var(
49+
--chat-panel-function-event-button-highlight-background-color
50+
);
4951
}
5052
}
5153

@@ -55,17 +57,29 @@
5557
}
5658

5759
.function-event-button-highlight {
58-
background-color: var(--chat-panel-function-event-button-highlight-background-color);
59-
border-color: var(--chat-panel-function-event-button-highlight-border-color) !important;
60+
background-color: var(
61+
--chat-panel-function-event-button-highlight-background-color
62+
);
63+
border-color: var(
64+
--chat-panel-function-event-button-highlight-border-color
65+
) !important;
6066
color: var(--chat-panel-function-event-button-highlight-color) !important;
6167
}
6268

69+
// Enables messages to have columns
70+
.message-column-container {
71+
display: flex;
72+
flex-direction: column;
73+
}
74+
6375
.user-message {
6476
display: flex;
6577
justify-content: flex-end;
6678
align-items: center;
6779
.message-card {
68-
background-color: var(--chat-panel-user-message-message-card-background-color);
80+
background-color: var(
81+
--chat-panel-user-message-message-card-background-color
82+
);
6983
align-self: flex-end;
7084
color: var(--chat-panel-user-message-message-card-color);
7185
box-shadow: none;
@@ -76,7 +90,9 @@
7690
display: flex;
7791
align-items: center;
7892
.message-card {
79-
background-color: var(--chat-panel-bot-message-message-card-background-color);
93+
background-color: var(
94+
--chat-panel-bot-message-message-card-background-color
95+
);
8096
align-self: flex-start;
8197
color: var(--chat-panel-bot-message-message-card-color);
8298
box-shadow: none;
@@ -85,8 +101,11 @@
85101

86102
.bot-message:focus-within {
87103
.message-card {
88-
background-color: var(--chat-panel-bot-message-focus-within-message-card-background-color);
89-
border: 1px solid var(--chat-panel-bot-message-focus-within-message-card-border-color);
104+
background-color: var(
105+
--chat-panel-bot-message-focus-within-message-card-background-color
106+
);
107+
border: 1px solid
108+
var(--chat-panel-bot-message-focus-within-message-card-border-color);
90109
}
91110
}
92111

@@ -153,7 +172,8 @@
153172

154173
.eval-response-header {
155174
padding-bottom: 5px;
156-
border-bottom: 2px solid var(--chat-panel-eval-response-header-border-bottom-color);
175+
border-bottom: 2px solid
176+
var(--chat-panel-eval-response-header-border-bottom-color);
157177
font-style: italic;
158178
font-weight: 700;
159179
}
@@ -278,7 +298,8 @@
278298
}
279299

280300
:host ::ng-deep .input-field .mat-mdc-text-field-wrapper {
281-
border: 1px solid var(--chat-panel-input-field-mat-mdc-text-field-wrapper-border-color);
301+
border: 1px solid
302+
var(--chat-panel-input-field-mat-mdc-text-field-wrapper-border-color);
282303
border-radius: 16px;
283304
}
284305

@@ -396,3 +417,22 @@ button.video-rec-btn {
396417
align-items: center;
397418
height: 100%;
398419
}
420+
421+
.feedback-buttons {
422+
// Default touch target is fixed at 48px, which is too large
423+
--mat-icon-button-touch-target-display: none;
424+
margin-left: 50px;
425+
426+
button {
427+
padding: 0;
428+
height: 24px;
429+
width: 24px;
430+
min-height: 24px;
431+
min-width: 24px;
432+
}
433+
mat-icon {
434+
font-size: 12px;
435+
height: 12px;
436+
width: 12px;
437+
}
438+
}

src/app/components/chat-panel/chat-panel.component.spec.ts

Lines changed: 85 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -23,41 +23,48 @@ import {fakeAsync, initTestBed, tick} from '../../testing/utils';
2323
import {MatDialogModule} from '@angular/material/dialog';
2424
import {By} from '@angular/platform-browser';
2525
import {NoopAnimationsModule} from '@angular/platform-browser/animations';
26-
import {of} from 'rxjs';
2726

27+
import {AGENT_SERVICE} from '../../core/services/interfaces/agent';
2828
import {FEATURE_FLAG_SERVICE} from '../../core/services/interfaces/feature-flag';
2929
import {STRING_TO_COLOR_SERVICE} from '../../core/services/interfaces/string-to-color';
30-
import {UI_STATE_SERVICE, UiStateService} from '../../core/services/interfaces/ui-state';
31-
import {MockFeatureFlagService} from '../../core/services/testing/mock-feature-flag.service';
32-
import {MockStringToColorService} from '../../core/services/testing/mock-string-to-color.service';
33-
import {MockUiStateService} from '../../core/services/testing/mock-ui-state.service';
30+
import {UI_STATE_SERVICE} from '../../core/services/interfaces/ui-state';
31+
import {
32+
MockFeatureFlagService
33+
} from '../../core/services/testing/mock-feature-flag.service';
34+
import {
35+
MockStringToColorService
36+
} from '../../core/services/testing/mock-string-to-color.service';
37+
import {
38+
MockUiStateService
39+
} from '../../core/services/testing/mock-ui-state.service';
3440
import {MARKDOWN_COMPONENT} from '../markdown/markdown.component.interface';
3541
import {MockMarkdownComponent} from '../markdown/testing/mock-markdown.component';
3642

3743
import {ChatPanelComponent} from './chat-panel.component';
38-
39-
const USER_ID = 'user';
40-
const FUNC1_NAME = 'func1';
44+
import {MockAgentService} from '../../core/services/testing/mock-agent.service';
4145

4246
describe('ChatPanelComponent', () => {
4347
let component: ChatPanelComponent;
4448
let fixture: ComponentFixture<ChatPanelComponent>;
4549
let mockFeatureFlagService: MockFeatureFlagService;
4650
let mockUiStateService: MockUiStateService;
4751
let mockStringToColorService: MockStringToColorService;
52+
let mockAgentService: MockAgentService;
4853

4954
beforeEach(async () => {
5055
mockFeatureFlagService = new MockFeatureFlagService();
5156
mockUiStateService = new MockUiStateService();
57+
mockAgentService = new MockAgentService();
5258

5359
mockFeatureFlagService.isMessageFileUploadEnabledResponse.next(true);
5460
mockFeatureFlagService.isManualStateUpdateEnabledResponse.next(true);
5561
mockFeatureFlagService.isBidiStreamingEnabledResponse.next(true);
62+
mockFeatureFlagService.isFeedbackServiceEnabledResponse.next(true);
5663

5764
mockStringToColorService = new MockStringToColorService();
5865
mockStringToColorService.stc.and.returnValue('rgb(255, 0, 0)');
5966

60-
initTestBed(); // required for 1p compat
67+
mockAgentService.getLoadingStateResponse.next(false);
6168

6269
initTestBed(); // required for 1p compat
6370
await TestBed
@@ -76,6 +83,7 @@ describe('ChatPanelComponent', () => {
7683
{provide: MARKDOWN_COMPONENT, useValue: MockMarkdownComponent},
7784
{provide: FEATURE_FLAG_SERVICE, useValue: mockFeatureFlagService},
7885
{provide: UI_STATE_SERVICE, useValue: mockUiStateService},
86+
{provide: AGENT_SERVICE, useValue: mockAgentService},
7987
],
8088
})
8189
.compileComponents();
@@ -476,4 +484,72 @@ describe('ChatPanelComponent', () => {
476484
});
477485
});
478486
});
487+
488+
describe('Feedback UI', () => {
489+
it('should show when feature flag is on', () => {
490+
component.messages = [{role: 'bot', text: 'message'}];
491+
492+
mockFeatureFlagService.isFeedbackServiceEnabledResponse.next(true);
493+
fixture.detectChanges();
494+
495+
let feedbackButtons = fixture.debugElement.query(By.css('.feedback-buttons'));
496+
expect(feedbackButtons).toBeTruthy();
497+
});
498+
499+
it('should hide when feature flag is off', () => {
500+
component.messages = [{role: 'bot', text: 'message'}];
501+
502+
mockFeatureFlagService.isFeedbackServiceEnabledResponse.next(false);
503+
fixture.detectChanges();
504+
505+
let feedbackButtons = fixture.debugElement.query(By.css('.feedback-buttons'));
506+
expect(feedbackButtons).toBeFalsy();
507+
});
508+
509+
it('should hide when agent response is loading', () => {
510+
component.messages = [{role: 'bot', text: 'message'}];
511+
512+
mockAgentService.getLoadingStateResponse.next(true);
513+
fixture.detectChanges();
514+
515+
const feedbackButtons = fixture.debugElement.query(By.css('.feedback-buttons'));
516+
expect(feedbackButtons).toBeFalsy();
517+
});
518+
519+
it('should show after each bot message', () => {
520+
component.messages = [
521+
{role: 'bot', text: 'message 1'},
522+
{role: 'bot', text: 'message 1'},
523+
{role: 'user', text: 'message 2'},
524+
{role: 'bot', text: 'message 1'},
525+
{role: 'bot', text: 'message 1'},
526+
];
527+
fixture.detectChanges();
528+
529+
let feedbackButtons = fixture.debugElement.queryAll(By.css('.feedback-buttons'));
530+
expect(feedbackButtons.length).toBe(4);
531+
});
532+
533+
it(`component should emit 'up' on up click`, () => {
534+
spyOn(component.feedback, 'emit');
535+
component.messages = [{role: 'bot', text: 'message'}];
536+
fixture.detectChanges();
537+
538+
const upButton = fixture.debugElement.queryAll(By.css('.feedback-buttons button'))[0];
539+
upButton.nativeElement.click();
540+
541+
expect(component.feedback.emit).toHaveBeenCalledWith({direction: 'up'});
542+
});
543+
544+
it(`component should emit 'down' on down click`, () => {
545+
spyOn(component.feedback, 'emit');
546+
component.messages = [{role: 'bot', text: 'message'}];
547+
fixture.detectChanges();
548+
549+
const downButton = fixture.debugElement.queryAll(By.css('.feedback-buttons button'))[1];
550+
downButton.nativeElement.click();
551+
552+
expect(component.feedback.emit).toHaveBeenCalledWith({direction: 'down'});
553+
});
554+
});
479555
});

src/app/components/chat-panel/chat-panel.component.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,8 @@ import {AudioPlayerComponent} from '../audio-player/audio-player.component';
4040
import {MARKDOWN_COMPONENT, MarkdownComponentInterface} from '../markdown/markdown.component.interface';
4141

4242
import {ChatPanelMessagesInjectionToken} from './chat-panel.component.i18n';
43+
import {AGENT_SERVICE} from '../../core/services/interfaces/agent';
44+
import {toSignal} from '@angular/core/rxjs-interop';
4345

4446
const ROOT_AGENT = 'root_agent';
4547

@@ -107,6 +109,9 @@ export class ChatPanelComponent implements OnChanges, AfterViewInit {
107109
@Output() readonly updateState = new EventEmitter<void>();
108110
@Output() readonly toggleAudioRecording = new EventEmitter<void>();
109111
@Output() readonly toggleVideoRecording = new EventEmitter<void>();
112+
@Output()
113+
readonly feedback =
114+
new EventEmitter<{direction: 'up'|'down'}>();
110115

111116
@ViewChild('videoContainer', {read: ElementRef}) videoContainer!: ElementRef;
112117
@ViewChild('autoScroll') scrollContainer!: ElementRef;
@@ -120,6 +125,7 @@ export class ChatPanelComponent implements OnChanges, AfterViewInit {
120125
MARKDOWN_COMPONENT,
121126
);
122127
private readonly featureFlagService = inject(FEATURE_FLAG_SERVICE);
128+
private readonly agentService = inject(AGENT_SERVICE);
123129
readonly MediaType = MediaType;
124130

125131
readonly isMessageFileUploadEnabledObs =
@@ -129,6 +135,8 @@ export class ChatPanelComponent implements OnChanges, AfterViewInit {
129135
readonly isBidiStreamingEnabledObs =
130136
this.featureFlagService.isBidiStreamingEnabled();
131137
readonly canEditSession = signal(true);
138+
readonly isUserFeedbackEnabled = toSignal(this.featureFlagService.isFeedbackServiceEnabled());
139+
readonly isLoadingAgentResponse = toSignal(this.agentService.getLoadingState());
132140

133141
constructor(private sanitizer: DomSanitizer) {}
134142

@@ -189,4 +197,8 @@ export class ChatPanelComponent implements OnChanges, AfterViewInit {
189197
renderGooglerSearch(content: string) {
190198
return this.sanitizer.bypassSecurityTrustHtml(content);
191199
}
200+
201+
emitFeedback(direction: 'up'|'down') {
202+
this.feedback.emit({direction});
203+
}
192204
}

0 commit comments

Comments
 (0)