Skip to content
Merged
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
61 changes: 56 additions & 5 deletions src/app/components/chat/chat.component.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ import {MARKDOWN_COMPONENT} from '../markdown/markdown.component.interface';
import {MockMarkdownComponent} from '../markdown/testing/mock-markdown.component';
import {SidePanelComponent} from '../side-panel/side-panel.component';

import {ChatComponent} from './chat.component';
import {ChatComponent, INITIAL_USER_INPUT_QUERY_PARAM} from './chat.component';

// Mock EvalTabComponent to satisfy the required viewChild in ChatComponent
@Component({
Expand Down Expand Up @@ -179,10 +179,13 @@ describe('ChatComponent', () => {

mockDialog = jasmine.createSpyObj('MatDialog', ['open']);
mockSnackBar = jasmine.createSpyObj('MatSnackBar', ['open']);
mockRouter = jasmine.createSpyObj('Router', ['navigate', 'createUrlTree'], {
events: of(new NavigationEnd(1, '', '')),
});
mockLocation = jasmine.createSpyObj('Location', ['replaceState']);
mockRouter = jasmine.createSpyObj(
'Router',
['navigate', 'createUrlTree', 'parseUrl', 'navigateByUrl'],
{events: of(new NavigationEnd(1, '', ''))},
);
mockRouter.parseUrl.and.returnValue({queryParams: {}} as any);
mockLocation = jasmine.createSpyObj('Location', ['replaceState', 'path']);
mockAgentBuilderService = jasmine.createSpyObj(
'AgentBuilderService', ['clear', 'setLoadedAgentData']);

Expand Down Expand Up @@ -267,6 +270,25 @@ describe('ChatComponent', () => {
expect(component).toBeTruthy();
});

it(
'should pre-fill user input from "q" query param only when app is selected',
fakeAsync(() => {
mockAgentService.setApp(''); // Initially no app
mockActivatedRoute.snapshot!
.queryParams = {[INITIAL_USER_INPUT_QUERY_PARAM]: 'hello'};
mockActivatedRoute.queryParams =
of({[INITIAL_USER_INPUT_QUERY_PARAM]: 'hello'});
fixture = TestBed.createComponent(ChatComponent);
component = fixture.componentInstance;
fixture.detectChanges();
expect(component.userInput).toBe(''); // Should be empty initially

mockAgentService.setApp(TEST_APP_1_NAME);
tick();

expect(component.userInput).toBe('hello'); // Should be set now
}));

it(
'should project content into adk-web-chat-container-top', () => {
const hostFixture = TestBed.createComponent(TestHostComponent);
Expand Down Expand Up @@ -912,6 +934,35 @@ describe('ChatComponent', () => {
.toContain(TEST_MESSAGE);
});

it(
'should clear "q" query param when message is sent',
fakeAsync(() => {
mockAgentService.setApp(''); // Initially no app
mockActivatedRoute.snapshot!
.queryParams = {[INITIAL_USER_INPUT_QUERY_PARAM]: 'hello'};
mockActivatedRoute.queryParams =
of({[INITIAL_USER_INPUT_QUERY_PARAM]: 'hello'});
const urlTree = {
queryParams: {[INITIAL_USER_INPUT_QUERY_PARAM]: 'hello'},
};
mockRouter.parseUrl.and.returnValue(urlTree as any);
mockLocation.path.and.returnValue('/?q=hello');
fixture = TestBed.createComponent(ChatComponent);
component = fixture.componentInstance;
fixture.detectChanges();
mockAgentService.setApp(TEST_APP_1_NAME);
tick();

component.sendMessage(new KeyboardEvent('keydown', {key: 'Enter'}));
tick();

expect(mockLocation.path).toHaveBeenCalled();
expect(mockRouter.parseUrl).toHaveBeenCalledWith('/?q=hello');
expect(urlTree.queryParams).toEqual({} as any); // q param should be
// deleted.
expect(mockRouter.navigateByUrl).toHaveBeenCalledWith(urlTree as any);
}));

describe('when event contains multiple text parts', () => {
it(
'should combine consecutive text parts into a single message',
Expand Down
25 changes: 25 additions & 0 deletions src/app/components/chat/chat.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,8 @@ import {ViewImageDialogComponent} from '../view-image-dialog/view-image-dialog.c
import {ChatMessagesInjectionToken} from './chat.component.i18n';

const ROOT_AGENT = 'root_agent';
/** Query parameter for pre-filling user input. */
export const INITIAL_USER_INPUT_QUERY_PARAM = 'q';

function fixBase64String(base64: string): string {
// Replace URL-safe characters if they exist
Expand Down Expand Up @@ -306,6 +308,25 @@ export class ChatComponent implements OnInit, AfterViewInit, OnDestroy {
this.syncSelectedAppFromUrl();
this.updateSelectedAppUrl();

combineLatest([
this.agentService.getApp(),
this.activatedRoute.queryParams,
])
.pipe(
filter(
([app, params]) =>
!!app && !!params[INITIAL_USER_INPUT_QUERY_PARAM],
),
first(),
map(([, params]) => params[INITIAL_USER_INPUT_QUERY_PARAM]))
.subscribe((initialUserInput) => {
// Use `setTimeout` to ensure the userInput is set after the current
// change detection cycle is complete.
setTimeout(() => {
this.userInput = initialUserInput;
});
});

this.streamChatService.onStreamClose().subscribe((closeReason) => {
const error =
'Please check server log for full details: \n' + closeReason;
Expand Down Expand Up @@ -570,6 +591,10 @@ export class ChatComponent implements OnInit, AfterViewInit, OnDestroy {
});
// Clear input
this.userInput = '';
// Clear the query param for the initial user input once it is sent.
const updatedUrl = this.router.parseUrl(this.location.path());
delete updatedUrl.queryParams[INITIAL_USER_INPUT_QUERY_PARAM];
await this.router.navigateByUrl(updatedUrl);
this.changeDetectorRef.detectChanges();
}

Expand Down
Loading