feat: infinite scroll in messages, add loader in sidebar
This commit is contained in:
parent
fad7658ead
commit
98cef31ae2
@ -20,8 +20,8 @@ export class ChatService {
|
||||
});
|
||||
}
|
||||
|
||||
public getMessages(id: string) {
|
||||
return this.http.get<MessageCollection>(`${this.apiUrl}/chats/${id}/messages`);
|
||||
public getMessages(id: string, page: number = 1) {
|
||||
return this.http.get<MessageCollection>(`${this.apiUrl}/chats/${id}/messages?page=${page}`);
|
||||
}
|
||||
|
||||
public getChats() {
|
||||
|
||||
@ -4,6 +4,13 @@
|
||||
class="flex-1 overflow-y-auto p-8 flex flex-col gap-6 scroll-smooth custom-scrollbar"
|
||||
#scrollContainer
|
||||
>
|
||||
<div appInfiniteScroll (scrolled)="loadMoreChats()">
|
||||
@if(messageStore.isLoadingPaginated()){
|
||||
<div class="w-full max-h-14 flex items-center justify-center">
|
||||
<app-loader class="w-8" />
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
@if(messageStore.isLoading()) {
|
||||
<div class="w-full h-full flex items-center justify-center">
|
||||
<app-loader class="w-8" />
|
||||
@ -15,7 +22,7 @@
|
||||
>
|
||||
<div
|
||||
class="px-5 py-4 rounded-2xl text-sm leading-relaxed shadow-[0_4px_15px_rgba(0,0,0,0.1)] relative whitespace-pre-wrap"
|
||||
[ngClass]="msg.attributes.role === 'user' ? 'bg-linear-to-br from-indigo-500 to-purple-600 text-white rounded-br-sm' : 'bg-white/10 border border-white/5 text-slate-200 rounded-bl-sm backdrop-blur-md'"
|
||||
[ngClass]="msg.attributes.role === 'user' ? 'bg-linear-to-br from-indigo-500 to-blue-600 text-white rounded-br-sm' : 'bg-white/10 border border-white/5 text-slate-200 rounded-bl-sm backdrop-blur-md'"
|
||||
>
|
||||
{{ msg.attributes.content }}
|
||||
</div>
|
||||
|
||||
@ -6,6 +6,7 @@ import { MessageState, Message, MessageResponse, ChatResponse, ChatState } from
|
||||
import { lastValueFrom } from 'rxjs';
|
||||
import { Router } from '@angular/router';
|
||||
import { JsonApiResource } from '../core/types/api';
|
||||
import { environment } from '../../environments/environment.development';
|
||||
|
||||
const initialMessageState: MessageState = {
|
||||
messages: [
|
||||
@ -22,6 +23,9 @@ const initialMessageState: MessageState = {
|
||||
],
|
||||
isLoading: true,
|
||||
isAgentThinking: false,
|
||||
isLoadingPaginated: false,
|
||||
currentPage: 1,
|
||||
lastPage: 1,
|
||||
id: null,
|
||||
};
|
||||
|
||||
@ -147,7 +151,11 @@ export const MessageStore = signalStore(
|
||||
try {
|
||||
const chatHistory = await lastValueFrom(chatService.getMessages(<string>store.id()));
|
||||
if (chatHistory.data.length !== 0) {
|
||||
patchState(store, { messages: chatHistory.data });
|
||||
patchState(store, {
|
||||
messages: chatHistory.data,
|
||||
currentPage: chatHistory.meta.current_page,
|
||||
lastPage: chatHistory.meta.last_page,
|
||||
});
|
||||
} else {
|
||||
patchState(store, { messages: initialMessageState.messages });
|
||||
}
|
||||
@ -173,6 +181,34 @@ export const MessageStore = signalStore(
|
||||
patchState(store, { isLoading: false });
|
||||
}
|
||||
},
|
||||
loadOlderMessages: async () => {
|
||||
if (
|
||||
store.isLoadingPaginated() ||
|
||||
store.isLoading() ||
|
||||
store.currentPage() >= store.lastPage() ||
|
||||
!store.id()
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
patchState(store, { isLoadingPaginated: true });
|
||||
const nextPage = store.currentPage() + 1;
|
||||
|
||||
try {
|
||||
const chatHistory = await lastValueFrom(
|
||||
chatService.getMessages(<string>store.id(), nextPage),
|
||||
);
|
||||
patchState(store, (state) => ({
|
||||
messages: [...chatHistory.data, ...state.messages],
|
||||
currentPage: chatHistory.meta.current_page,
|
||||
lastPage: chatHistory.meta.last_page,
|
||||
isLoadingPaginated: false,
|
||||
}));
|
||||
} catch (err) {
|
||||
if (!environment.production) console.error(err);
|
||||
patchState(store, { isLoadingPaginated: false });
|
||||
}
|
||||
},
|
||||
};
|
||||
}),
|
||||
);
|
||||
|
||||
@ -3,13 +3,15 @@ import { CommonModule } from '@angular/common';
|
||||
import { FormControl, ReactiveFormsModule } from '@angular/forms';
|
||||
import { ChatStore, MessageStore } from './chat.store';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import { Sidebar } from "../core/layout/sidebar/sidebar";
|
||||
import { Loader } from "../shared/loader/loader";
|
||||
import { Sidebar } from '../core/layout/sidebar/sidebar';
|
||||
import { Loader } from '../shared/loader/loader';
|
||||
import { InfiniteScroll } from '../core/directives/infinite-scroll';
|
||||
import { environment } from '../../environments/environment.development';
|
||||
|
||||
@Component({
|
||||
selector: 'app-chat',
|
||||
standalone: true,
|
||||
imports: [CommonModule, ReactiveFormsModule, Sidebar, Loader],
|
||||
imports: [CommonModule, ReactiveFormsModule, Sidebar, Loader, InfiniteScroll],
|
||||
templateUrl: './chat.html',
|
||||
styleUrl: './chat.css',
|
||||
})
|
||||
@ -24,8 +26,6 @@ export class Chat {
|
||||
constructor() {
|
||||
// Scroll to bottom when messages change
|
||||
effect(() => {
|
||||
// Accessing messages will trigger effect on change
|
||||
const msgs = this.messageStore.messages();
|
||||
const loading = this.messageStore.isLoading();
|
||||
|
||||
setTimeout(() => {
|
||||
@ -65,6 +65,12 @@ export class Chat {
|
||||
try {
|
||||
const el = this.scrollContainer.nativeElement;
|
||||
el.scrollTop = el.scrollHeight;
|
||||
} catch (err) {}
|
||||
} catch (err) {
|
||||
if (!environment.production) console.log(err);
|
||||
}
|
||||
}
|
||||
|
||||
protected loadMoreChats() {
|
||||
this.messageStore.loadOlderMessages();
|
||||
}
|
||||
}
|
||||
|
||||
@ -24,6 +24,9 @@ export type MessageState = {
|
||||
messages: JsonApiResource<Message>[];
|
||||
isLoading: boolean;
|
||||
isAgentThinking: boolean;
|
||||
isLoadingPaginated: boolean;
|
||||
currentPage: number;
|
||||
lastPage: number;
|
||||
id: string | null;
|
||||
};
|
||||
export type ChatState = {
|
||||
|
||||
8
frontend/src/app/core/directives/infinite-scroll.spec.ts
Normal file
8
frontend/src/app/core/directives/infinite-scroll.spec.ts
Normal file
@ -0,0 +1,8 @@
|
||||
import { InfiniteScroll } from './infinite-scroll';
|
||||
|
||||
describe('InfiniteScroll', () => {
|
||||
it('should create an instance', () => {
|
||||
const directive = new InfiniteScroll();
|
||||
expect(directive).toBeTruthy();
|
||||
});
|
||||
});
|
||||
26
frontend/src/app/core/directives/infinite-scroll.ts
Normal file
26
frontend/src/app/core/directives/infinite-scroll.ts
Normal file
@ -0,0 +1,26 @@
|
||||
import { DestroyRef, Directive, ElementRef, inject, output } from '@angular/core';
|
||||
|
||||
@Directive({
|
||||
selector: '[appInfiniteScroll]',
|
||||
})
|
||||
export class InfiniteScroll {
|
||||
scrolled = output<void>();
|
||||
private elementRef = inject(ElementRef);
|
||||
private destroyRef = inject(DestroyRef);
|
||||
private observer?: IntersectionObserver;
|
||||
|
||||
constructor() {
|
||||
this.observer = new IntersectionObserver(
|
||||
([entry]) => {
|
||||
if (entry.isIntersecting) {
|
||||
this.scrolled.emit();
|
||||
}
|
||||
},
|
||||
{ root: null, rootMargin: '0px', threshold: 0.1 },
|
||||
);
|
||||
this.observer.observe(this.elementRef.nativeElement);
|
||||
this.destroyRef.onDestroy(() => {
|
||||
this.observer?.disconnect();
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -150,6 +150,13 @@
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
<!--Loading state-->
|
||||
@if(chatStore.isLoading()){
|
||||
<div class="flex flex-1 h-full flex-col items-center justify-center py-10 px-4">
|
||||
<app-loader class="w-12" />
|
||||
</div>
|
||||
}
|
||||
<!-- Empty state -->
|
||||
@if (chatStore.chats().length === 0) {
|
||||
<div class="flex flex-1 flex-col items-center justify-center py-10 px-4 text-center">
|
||||
|
||||
@ -1,17 +1,18 @@
|
||||
import { Component, signal, computed, inject, OnInit } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { RouterModule } from '@angular/router';
|
||||
import { ActivatedRoute, RouterModule } from '@angular/router';
|
||||
import { ChatStore, MessageStore } from '../../../chat/chat.store';
|
||||
import { AuthStore } from '../../../auth/auth.store';
|
||||
import { InitialsPipe } from './initials-pipe';
|
||||
import { JsonApiResource } from '../../types/api';
|
||||
import { Chat } from '../../../chat/chat.types';
|
||||
import { Login } from '../../../auth/login/login';
|
||||
import { Loader } from "../../../shared/loader/loader";
|
||||
|
||||
@Component({
|
||||
selector: 'app-sidebar',
|
||||
standalone: true,
|
||||
imports: [CommonModule, RouterModule, InitialsPipe],
|
||||
imports: [CommonModule, RouterModule, InitialsPipe, Loader],
|
||||
templateUrl: 'sidebar.html',
|
||||
styleUrl: 'sidebar.css',
|
||||
})
|
||||
@ -19,6 +20,7 @@ export class Sidebar implements OnInit {
|
||||
protected chatStore = inject(ChatStore);
|
||||
protected messageStore = inject(MessageStore);
|
||||
protected authStore = inject(AuthStore);
|
||||
private route = inject(ActivatedRoute);
|
||||
|
||||
protected isOpen = signal(true);
|
||||
protected isCogMenuOpen = signal(false);
|
||||
@ -27,6 +29,12 @@ export class Sidebar implements OnInit {
|
||||
|
||||
ngOnInit() {
|
||||
this.chatStore.fetchChats();
|
||||
this.route.paramMap.subscribe((params) => {
|
||||
const urlId = params.get('id');
|
||||
if (urlId) {
|
||||
this.activeChatId.set(urlId);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
protected sidebarClasses = computed(() => {
|
||||
@ -46,7 +54,7 @@ export class Sidebar implements OnInit {
|
||||
}
|
||||
|
||||
protected chatIconClasses(chat: any): string {
|
||||
const base = 'w-6 h-6 rounded-md flex items-center justify-center shrink-0 mt-0.5';
|
||||
const base = 'w-6 h-6 rounded-md flex items-center justify-center shrink-0 mt-0.5 pt-0.5';
|
||||
return chat.isActive
|
||||
? base + ' bg-[#2d5be3]/20 text-[#5b8af0]'
|
||||
: base + ' bg-[#111a2e] text-[#3a5272] group-hover:bg-[#1a2a48] group-hover:text-[#5a80a8]';
|
||||
@ -66,7 +74,6 @@ export class Sidebar implements OnInit {
|
||||
|
||||
protected selectChat(chatId: string): void {
|
||||
this.activeChatId.set(chatId);
|
||||
console.log(chatId);
|
||||
this.messageStore.fetchChatHistory(chatId);
|
||||
}
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user