1717
1818import { TextFieldModule } from '@angular/cdk/text-field' ;
1919import { 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' ;
2122import { FormsModule } from '@angular/forms' ;
2223import { MatButtonModule } from '@angular/material/button' ;
2324import { MatCardModule } from '@angular/material/card' ;
@@ -30,18 +31,21 @@ import {MatProgressSpinnerModule} from '@angular/material/progress-spinner';
3031import { MatTooltipModule } from '@angular/material/tooltip' ;
3132import { DomSanitizer , SafeHtml } from '@angular/platform-browser' ;
3233import { NgxJsonViewerModule } from 'ngx-json-viewer' ;
34+ import { EMPTY , NEVER , of , Subject } from 'rxjs' ;
35+ import { catchError , first , switchMap , tap } from 'rxjs/operators' ;
3336
3437import type { EvalCase } from '../../core/models/Eval' ;
38+ import { AGENT_SERVICE } from '../../core/services/interfaces/agent' ;
3539import { FEATURE_FLAG_SERVICE } from '../../core/services/interfaces/feature-flag' ;
40+ import { SESSION_SERVICE } from '../../core/services/interfaces/session' ;
3641import { STRING_TO_COLOR_SERVICE } from '../../core/services/interfaces/string-to-color' ;
42+ import { ListResponse } from '../../core/services/interfaces/types' ;
3743import { UI_STATE_SERVICE } from '../../core/services/interfaces/ui-state' ;
3844import { MediaType , } from '../artifact-tab/artifact-tab.component' ;
3945import { AudioPlayerComponent } from '../audio-player/audio-player.component' ;
4046import { MARKDOWN_COMPONENT , MarkdownComponentInterface } from '../markdown/markdown.component.interface' ;
4147
4248import { ChatPanelMessagesInjectionToken } from './chat-panel.component.i18n' ;
43- import { AGENT_SERVICE } from '../../core/services/interfaces/agent' ;
44- import { toSignal } from '@angular/core/rxjs-interop' ;
4549
4650const ROOT_AGENT = 'root_agent' ;
4751
@@ -70,6 +74,7 @@ const ROOT_AGENT = 'root_agent';
7074} )
7175export 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 ) {
0 commit comments