Skip to content

Commit 72e1ec4

Browse files
google-genai-botcopybara-github
authored andcommitted
ADK changes
PiperOrigin-RevId: 841664815
1 parent c40f1b4 commit 72e1ec4

16 files changed

+439
-106
lines changed

package-lock.json

Lines changed: 15 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

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

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,12 @@
1616

1717
@let isSessionLoading = uiStateService.isSessionLoading() | async;
1818
@if (appName != "" && !isSessionLoading) {
19-
<div #autoScroll class="chat-messages">
19+
<div #autoScroll class="chat-messages" (scroll)="onScroll.next($event)">
20+
@if(uiStateService.isMessagesLoading() | async) {
21+
<div class="messages-loading-container">
22+
<mat-progress-bar mode="indeterminate"></mat-progress-bar>
23+
</div>
24+
}
2025
<div #videoContainer></div>
2126
@for (message of messages; track message; let i = $index) {
2227
<div class="message-column-container">

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -436,3 +436,8 @@ button.video-rec-btn {
436436
width: 12px;
437437
}
438438
}
439+
440+
.messages-loading-container {
441+
margin-top: 1em;
442+
margin-bottom: 1em;
443+
}

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

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,18 +19,23 @@ import {HttpClientTestingModule} from '@angular/common/http/testing';
1919
import {SimpleChange} from '@angular/core';
2020
import {ComponentFixture, TestBed} from '@angular/core/testing';
2121
// 1p-ONLY-IMPORTS: import {beforeEach, describe, expect, it}
22+
import {of} from 'rxjs';
2223
import {fakeAsync, initTestBed, tick} from '../../testing/utils';
2324
import {MatDialogModule} from '@angular/material/dialog';
2425
import {By} from '@angular/platform-browser';
2526
import {NoopAnimationsModule} from '@angular/platform-browser/animations';
2627

2728
import {AGENT_SERVICE} from '../../core/services/interfaces/agent';
2829
import {FEATURE_FLAG_SERVICE} from '../../core/services/interfaces/feature-flag';
30+
import {SESSION_SERVICE} from '../../core/services/interfaces/session';
2931
import {STRING_TO_COLOR_SERVICE} from '../../core/services/interfaces/string-to-color';
3032
import {UI_STATE_SERVICE} from '../../core/services/interfaces/ui-state';
3133
import {
3234
MockFeatureFlagService
3335
} from '../../core/services/testing/mock-feature-flag.service';
36+
import {
37+
MockSessionService
38+
} from '../../core/services/testing/mock-session.service';
3439
import {
3540
MockStringToColorService
3641
} from '../../core/services/testing/mock-string-to-color.service';
@@ -50,16 +55,19 @@ describe('ChatPanelComponent', () => {
5055
let mockUiStateService: MockUiStateService;
5156
let mockStringToColorService: MockStringToColorService;
5257
let mockAgentService: MockAgentService;
58+
let mockSessionService: MockSessionService;
5359

5460
beforeEach(async () => {
5561
mockFeatureFlagService = new MockFeatureFlagService();
5662
mockUiStateService = new MockUiStateService();
5763
mockAgentService = new MockAgentService();
64+
mockSessionService = new MockSessionService();
5865

5966
mockFeatureFlagService.isMessageFileUploadEnabledResponse.next(true);
6067
mockFeatureFlagService.isManualStateUpdateEnabledResponse.next(true);
6168
mockFeatureFlagService.isBidiStreamingEnabledResponse.next(true);
6269
mockFeatureFlagService.isFeedbackServiceEnabledResponse.next(true);
70+
mockFeatureFlagService.isInfinityMessageScrollingEnabledResponse.next(true);
6371

6472
mockStringToColorService = new MockStringToColorService();
6573
mockStringToColorService.stc.and.returnValue('rgb(255, 0, 0)');
@@ -84,6 +92,7 @@ describe('ChatPanelComponent', () => {
8492
{provide: FEATURE_FLAG_SERVICE, useValue: mockFeatureFlagService},
8593
{provide: UI_STATE_SERVICE, useValue: mockUiStateService},
8694
{provide: AGENT_SERVICE, useValue: mockAgentService},
95+
{provide: SESSION_SERVICE, useValue: mockSessionService},
8796
],
8897
})
8998
.compileComponents();
@@ -364,6 +373,54 @@ describe('ChatPanelComponent', () => {
364373
expect(component.scrollInterrupted).toBeFalse();
365374
expect(scrollContainerElement.scrollTo).toHaveBeenCalled();
366375
}));
376+
377+
it(
378+
'should call uiStateService.lazyLoadMessages when scrolled to top',
379+
fakeAsync(() => {
380+
// Given
381+
const initialMessageCount = 50;
382+
const initialMessages = Array.from(
383+
{length: initialMessageCount},
384+
(_, i) => ({role: 'bot', text: `message ${i}`}));
385+
component.messages = initialMessages;
386+
fixture.detectChanges();
387+
388+
const scrollContainerElement = component.scrollContainer.nativeElement;
389+
// Make sure the scroll height is greater than the client height
390+
scrollContainerElement.style.height = '100px';
391+
scrollContainerElement.style.overflow = 'auto';
392+
scrollContainerElement.scrollTop = 100;
393+
fixture.detectChanges();
394+
395+
// Initialize nextPageToken to allow loading more messages
396+
mockUiStateService.newMessagesLoadedResponse.next(
397+
{items: [], nextPageToken: 'initial-token'});
398+
tick();
399+
400+
// When
401+
scrollContainerElement.scrollTop = 0;
402+
scrollContainerElement.dispatchEvent(new Event('scroll'));
403+
tick(200); // Wait for debounce
404+
405+
// Then
406+
expect(mockUiStateService.lazyLoadMessages).toHaveBeenCalled();
407+
408+
mockUiStateService.lazyLoadMessagesResponse.next();
409+
410+
// When more messages are loaded
411+
const newMessages =
412+
Array.from({length: 20}, (_, i) => ({role: 'bot', text: `new ${i}`}));
413+
component.messages = [...newMessages, ...component.messages];
414+
mockUiStateService.newMessagesLoadedResponse.next(
415+
{items: newMessages, nextPageToken: 'next'});
416+
tick();
417+
fixture.detectChanges();
418+
419+
// Then
420+
expect(component.messages.length)
421+
.toBe(initialMessageCount + newMessages.length);
422+
expect(component.messages[0]).toEqual(newMessages[0]);
423+
}));
367424
});
368425

369426
describe('disabled features', () => {

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

Lines changed: 78 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,8 @@
1717

1818
import {TextFieldModule} from '@angular/cdk/text-field';
1919
import {CommonModule, NgClass} from '@angular/common';
20-
import {AfterViewInit, Component, ElementRef, EventEmitter, inject, Input, OnChanges, Output, signal, SimpleChanges, Type, ViewChild} from '@angular/core';
20+
import {AfterViewInit, Component, DestroyRef, effect, ElementRef, EventEmitter, inject, input, Input, OnChanges, Output, signal, SimpleChanges, Type, ViewChild} from '@angular/core';
21+
import {takeUntilDestroyed, toSignal} from '@angular/core/rxjs-interop';
2122
import {FormsModule} from '@angular/forms';
2223
import {MatButtonModule} from '@angular/material/button';
2324
import {MatCardModule} from '@angular/material/card';
@@ -30,18 +31,21 @@ import {MatProgressSpinnerModule} from '@angular/material/progress-spinner';
3031
import {MatTooltipModule} from '@angular/material/tooltip';
3132
import {DomSanitizer, SafeHtml} from '@angular/platform-browser';
3233
import {NgxJsonViewerModule} from 'ngx-json-viewer';
34+
import {EMPTY, NEVER, of, Subject} from 'rxjs';
35+
import {catchError, first, switchMap, tap} from 'rxjs/operators';
3336

3437
import type {EvalCase} from '../../core/models/Eval';
38+
import {AGENT_SERVICE} from '../../core/services/interfaces/agent';
3539
import {FEATURE_FLAG_SERVICE} from '../../core/services/interfaces/feature-flag';
40+
import {SESSION_SERVICE} from '../../core/services/interfaces/session';
3641
import {STRING_TO_COLOR_SERVICE} from '../../core/services/interfaces/string-to-color';
42+
import {ListResponse} from '../../core/services/interfaces/types';
3743
import {UI_STATE_SERVICE} from '../../core/services/interfaces/ui-state';
3844
import {MediaType,} from '../artifact-tab/artifact-tab.component';
3945
import {AudioPlayerComponent} from '../audio-player/audio-player.component';
4046
import {MARKDOWN_COMPONENT, MarkdownComponentInterface} from '../markdown/markdown.component.interface';
4147

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

4650
const ROOT_AGENT = 'root_agent';
4751

@@ -70,6 +74,7 @@ const ROOT_AGENT = 'root_agent';
7074
})
7175
export class ChatPanelComponent implements OnChanges, AfterViewInit {
7276
@Input() appName: string = '';
77+
sessionName = input<string>('');
7378
@Input() messages: any[] = [];
7479
@Input() isChatMode: boolean = true;
7580
@Input() evalCase: EvalCase|null = null;
@@ -109,15 +114,14 @@ export class ChatPanelComponent implements OnChanges, AfterViewInit {
109114
@Output() readonly updateState = new EventEmitter<void>();
110115
@Output() readonly toggleAudioRecording = new EventEmitter<void>();
111116
@Output() readonly toggleVideoRecording = new EventEmitter<void>();
112-
@Output()
113-
readonly feedback =
114-
new EventEmitter<{direction: 'up'|'down'}>();
117+
@Output() readonly feedback = new EventEmitter<{direction: 'up' | 'down'}>();
115118

116119
@ViewChild('videoContainer', {read: ElementRef}) videoContainer!: ElementRef;
117120
@ViewChild('autoScroll') scrollContainer!: ElementRef;
118121
@ViewChild('messageTextarea') public textarea: ElementRef|undefined;
119122
scrollInterrupted = false;
120123
private previousMessageCount = 0;
124+
private nextPageToken = '';
121125
protected readonly i18n = inject(ChatPanelMessagesInjectionToken);
122126
protected readonly uiStateService = inject(UI_STATE_SERVICE);
123127
private readonly stringToColorService = inject(STRING_TO_COLOR_SERVICE);
@@ -126,6 +130,8 @@ export class ChatPanelComponent implements OnChanges, AfterViewInit {
126130
);
127131
private readonly featureFlagService = inject(FEATURE_FLAG_SERVICE);
128132
private readonly agentService = inject(AGENT_SERVICE);
133+
private readonly sessionService = inject(SESSION_SERVICE);
134+
private readonly destroyRef = inject(DestroyRef);
129135
readonly MediaType = MediaType;
130136

131137
readonly isMessageFileUploadEnabledObs =
@@ -135,10 +141,73 @@ export class ChatPanelComponent implements OnChanges, AfterViewInit {
135141
readonly isBidiStreamingEnabledObs =
136142
this.featureFlagService.isBidiStreamingEnabled();
137143
readonly canEditSession = signal(true);
138-
readonly isUserFeedbackEnabled = toSignal(this.featureFlagService.isFeedbackServiceEnabled());
139-
readonly isLoadingAgentResponse = toSignal(this.agentService.getLoadingState());
144+
readonly isUserFeedbackEnabled =
145+
toSignal(this.featureFlagService.isFeedbackServiceEnabled());
146+
readonly isLoadingAgentResponse =
147+
toSignal(this.agentService.getLoadingState());
148+
149+
protected readonly onScroll = new Subject<Event>();
150+
151+
constructor(private sanitizer: DomSanitizer) {
152+
effect(() => {
153+
const sessionName = this.sessionName();
154+
if (sessionName) {
155+
this.nextPageToken = '';
156+
this.uiStateService
157+
.lazyLoadMessages(sessionName, {
158+
pageSize: 100,
159+
pageToken: this.nextPageToken,
160+
})
161+
.pipe(first())
162+
.subscribe();
163+
}
164+
});
165+
}
166+
167+
ngOnInit() {
168+
if (this.featureFlagService.isInfinityMessageScrollingEnabled()) {
169+
this.uiStateService.onNewMessagesLoaded()
170+
.pipe(takeUntilDestroyed(this.destroyRef))
171+
.subscribe((response: ListResponse<any>) => {
172+
this.nextPageToken = response.nextPageToken ?? '';
173+
// Scroll to the last unseen message after the new messages are
174+
// loaded.
175+
if (this.scrollContainer?.nativeElement) {
176+
const oldScrollHeight =
177+
this.scrollContainer.nativeElement.scrollHeight;
178+
setTimeout(() => {
179+
const newScrollHeight =
180+
this.scrollContainer.nativeElement.scrollHeight;
181+
this.scrollContainer.nativeElement.scrollTop =
182+
newScrollHeight - oldScrollHeight;
183+
});
184+
}
185+
});
186+
187+
this.onScroll
188+
.pipe(
189+
takeUntilDestroyed(this.destroyRef),
190+
switchMap((event: Event) => {
191+
const element = event.target as HTMLElement;
192+
if (element.scrollTop !== 0) {
193+
return EMPTY;
194+
}
140195

141-
constructor(private sanitizer: DomSanitizer) {}
196+
if (!this.nextPageToken) {
197+
return EMPTY;
198+
}
199+
200+
return this.uiStateService
201+
.lazyLoadMessages(this.sessionName(), {
202+
pageSize: 100,
203+
pageToken: this.nextPageToken,
204+
})
205+
.pipe(first(), catchError(() => NEVER));
206+
}),
207+
)
208+
.subscribe();
209+
}
210+
}
142211

143212
ngAfterViewInit() {
144213
if (this.scrollContainer?.nativeElement) {

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -316,6 +316,7 @@
316316
(updateState)="updateState()"
317317
(toggleAudioRecording)="toggleAudioRecording()"
318318
(toggleVideoRecording)="toggleVideoRecording()"
319+
[sessionName]="sessionId"
319320
></app-chat-panel>
320321
}
321322
</mat-card>

0 commit comments

Comments
 (0)