8000 fix(chat): display human input replies in the transcript by jabrailkhalil · Pull Request #539 · google/adk-web · GitHub
[go: up one dir, main page]

Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions src/app/components/event-content/event-content.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,13 @@
(openBase64InNewTab)="openBase64InNewTab.emit($event)"
></app-content-bubble>
}
@if (uiEvent.role === 'user') {
@for (functionResponse of uiEvent.functionResponses; track functionResponse) {
@if (functionResponse.name === 'adk_request_input' && functionResponse.response) {
<pre class="user-input-response">{{ getUserInputResponseText(functionResponse) }}</pre>
}
}
}
@if (hasGroundingSources(uiEvent)) {
<app-grounding-sources [groundingMetadata]="uiEvent.event.groundingMetadata"></app-grounding-sources>
}
Expand Down
18 changes: 17 additions & 1 deletion src/app/components/event-content/event-content.component.scss
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,22 @@ app-content-bubble+app-content-bubble {
margin-top: 5px;
}

.user-input-response {
align-self: flex-end;
box-sizing: border-box;
max-width: 80%;
margin: 5px 0;
padding: 5px 20px;
border-radius: 20px;
color: var(--mat-sys-on-primary-container);
background-color: var(--mat-sys-primary-container);
font: inherit;
font-size: 14px;
font-weight: 400;
white-space: pre-wrap;
overflow-wrap: anywhere;
}

.event-chips-container {
display: flex;
flex-wrap: wrap;
Expand Down Expand Up @@ -114,4 +130,4 @@ html.light-theme ::ng-deep .performance-warning-btn {
box-shadow: 0 2px 6px rgba(230, 81, 0, 0.15);
}
}
}
}
88 changes: 88 additions & 0 deletions src/app/components/event-content/event-content.component.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,94 @@ describe('EventContentComponent', () => {
});
});

describe('Human input responses', () => {
function renderResponses(
responses: Array<{name: string, response: any}>, role = 'user') {
component.uiEvent = new UiEvent({
role,
event: {id: 'human-response', author: role} as AdkEvent,
functionResponses: responses.map((response, index) => ({
id: `input-${index}`,
...response,
})),
});
component.index = 0;
fixture.detectChanges();
return fixture.debugElement.queryAll(By.css('.user-input-response'));
}

it('shows a free-text answer while keeping its function response chip', () => {
const responses = renderResponses([
{name: 'adk_request_input', response: {result: 'yes'}},
]);

expect(responses.length).toBe(1);
expect(responses[0]?.nativeElement.textContent).toBe('yes');
const chip = fixture.debugElement.query(By.css('app-hover-info-button'));
expect(chip.componentInstance.text).toBe('adk_request_input');
expect(chip.componentInstance.tooltipContent).toEqual({result: 'yes'});
});

it('preserves multiline text and displays markup literally', () => {
const answer = '<img src=x this literal**';
const responses = renderResponses([
{name: 'adk_request_input', response: {result: answer}},
]);

expect(responses[0]?.nativeElement.textContent).toBe(answer);
expect(responses[0]?.query(By.css('img'))).toBeNull();
expect(responses[0]?.query(By.css('strong'))).toBeNull();
});

it('shows structured form answers without dropping false, zero or null', () => {
const answer = {approved: false, retries: 0, note: null, labels: ['a', 'b']};
const responses = renderResponses([
{name: 'adk_request_input', response: answer},
]);

expect(responses[0]?.nativeElement.textContent)
.toBe(JSON.stringify(answer, null, 2));
});

it('keeps additional fields when a structured answer has a result field', () => {
const answer = {result: 'yes', reason: 'Reviewed the proposed change'};
const responses = renderResponses([
{name: 'adk_request_input', response: answer},
]);

expect(responses[0]?.nativeElement.textContent)
.toBe(JSON.stringify(answer, null, 2));
});

it('shows every human input answer in the order it was sent', () => {
const responses = renderResponses([
{name: 'adk_request_input', response: {result: 'first'}},
{name: 'adk_request_input', response: {result: 'second'}},
]);

expect(responses.map(response => response.nativeElement.textContent))
.toEqual(['first', 'second']);
});

it('does not present an agent tool result as a human answer', () => {
const responses = renderResponses([
{name: 'adk_request_input', response: {result: 'tool output'}},
], 'bot');

expect(responses.length).toBe(0);
});

it('leaves other function responses in their existing chip format', () => {
const responses = renderResponses([
{name: 'get_weather', response: {result: 'sunny'}},
]);

expect(responses.length).toBe(0);
const chip = fixture.debugElement.query(By.css('app-hover-info-button'));
expect(chip.componentInstance.text).toBe('get_weather');
});
});

describe('Shell commands', () => {
it('renders a shell command call under its function call chip', () => {
component.uiEvent = new UiEvent({
Expand Down
12 changes: 12 additions & 0 deletions src/app/components/event-content/event-content.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,18 @@ export class EventContentComponent {
return isSkillToolResponse(functionResponse);
}

/** Displays the answer sent by the human input form without hiding fields. */
getUserInputResponseText(functionResponse: FunctionResponse): string {
const response = functionResponse.response;
// Free-text submissions are wrapped in {result: text}; schema-based forms
// send their fields directly, including any field named "result".
if (Object.keys(response).length === 1 &&
typeof response['result'] === 'string') {
return response['result'];
}
return JSON.stringify(response, null, 2);
}

/** Whether a call asks the user to approve another tool call. */
isConfirmationRequest(functionCall: FunctionCall): boolean {
return getConfirmationRequest(functionCall) !== null;
Expand Down
Loading
0