chore: add loader, fix chat states

This commit is contained in:
kushal-saha 2026-05-04 06:19:56 +00:00
parent 9853c6128c
commit fad7658ead
8 changed files with 97 additions and 18 deletions

View File

@ -5,9 +5,9 @@
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;
Route::get("/me", function (Request $request) {
Route::get('/me', function (Request $request) {
return $request->user();
})->middleware("auth:sanctum");
})->middleware('auth:sanctum');
Route::apiResource("chats", ChatController::class);
Route::apiResource("chats.messages", ChatMessageController::class);
Route::apiResource('chats', ChatController::class);
Route::apiResource('chats.messages', ChatMessageController::class);

View File

@ -4,7 +4,11 @@
class="flex-1 overflow-y-auto p-8 flex flex-col gap-6 scroll-smooth custom-scrollbar"
#scrollContainer
>
@for (msg of messageStore.messages(); track msg.id) {
@if(messageStore.isLoading()) {
<div class="w-full h-full flex items-center justify-center">
<app-loader class="w-8" />
</div>
} @else{ @for (msg of messageStore.messages(); track msg.id) {
<div
class="flex flex-col max-w-[80%] animate-[messageAppear_0.5s_cubic-bezier(0.16,1,0.3,1)]"
[ngClass]="msg.attributes.role === 'user' ? 'self-end items-end' : 'self-start items-start'"
@ -19,7 +23,9 @@
{{ msg.attributes.createdAt | date:'shortTime' }}
</div>
</div>
} @if (messageStore.isLoading()) {
}
<!--Agent thinking indicator-->
@if (messageStore.isAgentThinking()) {
<div
class="flex items-center gap-1.5 px-5 py-4 bg-white/5 rounded-2xl rounded-bl-sm self-start animate-[fadeIn_0.3s_ease-in-out]"
>
@ -33,7 +39,7 @@
class="w-2 h-2 bg-[#4facfe] rounded-full animate-[typing_1.4s_infinite_ease-in-out_both] delay-0"
></div>
</div>
}
} }
</div>
<div class="px-8 py-3">

View File

@ -20,7 +20,8 @@ const initialMessageState: MessageState = {
},
},
],
isLoading: false,
isLoading: true,
isAgentThinking: false,
id: null,
};
@ -37,14 +38,16 @@ export const MessageStore = signalStore(
const router = inject(Router);
const newChat = async () => {
patchState(store, { isLoading: true });
try {
const response = await lastValueFrom(chatService.newChat());
const chatId = response.data.id;
patchState(store, { id: chatId });
patchState(store, { id: chatId, isLoading: false });
await router.navigate(['/', chatId], { replaceUrl: true });
return chatId;
} catch (error) {
patchState(store, { isLoading: false });
console.error('Failed to create a new chat:', error);
throw error;
}
@ -98,7 +101,7 @@ export const MessageStore = signalStore(
patchState(store, (state) => ({
messages: [...state.messages, userMessage],
isLoading: true,
isAgentThinking: true,
}));
try {
const aiMessage: MessageResponse = await lastValueFrom(
@ -107,7 +110,7 @@ export const MessageStore = signalStore(
patchState(store, (state) => ({
messages: [...state.messages, aiMessage.data],
isLoading: false,
isAgentThinking: false,
}));
} catch (error: any) {
let errorText = 'Sorry, I encountered an error while communicating with the server.';
@ -125,20 +128,33 @@ export const MessageStore = signalStore(
}
setErrorMessage(errorText, 'assistant');
patchState(store, () => ({
isLoading: false,
isAgentThinking: false,
}));
}
},
fetchChatHistory: async () => {
if (!store.id()) {
fetchChatHistory: async (chatId: string | null = null) => {
if (!store.id() && !chatId) {
setErrorMessage('Please create a new chat session first.', 'assistant');
return;
}
if (chatId) {
patchState(store, { id: chatId });
}
patchState(store, { isLoading: true });
try {
const chatHistory = await lastValueFrom(chatService.getMessages(<string>store.id()));
patchState(store, { messages: chatHistory.data, isLoading: false });
if (chatHistory.data.length !== 0) {
patchState(store, { messages: chatHistory.data });
} else {
patchState(store, { messages: initialMessageState.messages });
}
patchState(store, { isLoading: false });
if (chatId) {
await router.navigate(['/', chatId], { replaceUrl: true });
}
} catch (error: any) {
let errorText = 'Failed to load chat history. Please try again.';

View File

@ -4,11 +4,12 @@ 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";
@Component({
selector: 'app-chat',
standalone: true,
imports: [CommonModule, ReactiveFormsModule, Sidebar],
imports: [CommonModule, ReactiveFormsModule, Sidebar, Loader],
templateUrl: './chat.html',
styleUrl: './chat.css',
})

View File

@ -23,6 +23,7 @@ export type MessageCollection = JsonApiCollection<Message>;
export type MessageState = {
messages: JsonApiResource<Message>[];
isLoading: boolean;
isAgentThinking: boolean;
id: string | null;
};
export type ChatState = {

View File

@ -1,11 +1,12 @@
import { Component, signal, computed, inject, OnInit } from '@angular/core';
import { CommonModule } from '@angular/common';
import { RouterModule } from '@angular/router';
import { ChatStore } from '../../../chat/chat.store';
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';
@Component({
selector: 'app-sidebar',
@ -16,6 +17,7 @@ import { Chat } from '../../../chat/chat.types';
})
export class Sidebar implements OnInit {
protected chatStore = inject(ChatStore);
protected messageStore = inject(MessageStore);
protected authStore = inject(AuthStore);
protected isOpen = signal(true);
@ -59,11 +61,13 @@ export class Sidebar implements OnInit {
}
protected newChat(): void {
console.log('New chat triggered');
this.messageStore.newChat();
}
protected selectChat(chatId: string): void {
this.activeChatId.set(chatId);
console.log(chatId);
this.messageStore.fetchChatHistory(chatId);
}
protected onSearch(event: Event): void {

View File

@ -0,0 +1,22 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { Loader } from './loader';
describe('Loader', () => {
let component: Loader;
let fixture: ComponentFixture<Loader>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [Loader],
}).compileComponents();
fixture = TestBed.createComponent(Loader);
component = fixture.componentInstance;
await fixture.whenStable();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,29 @@
import { Component } from '@angular/core';
@Component({
selector: 'app-loader',
imports: [],
template: `<div id="loader"></div>`,
styles: `
#loader {
max-width: 40px;
aspect-ratio: 1;
border-radius: 50%;
border: 4px solid lightblue;
border-right-color: var(--color-blue-500);
animation: spinner 1s infinite linear;
}
@keyframes spinner {
to {
transform: rotate(1turn);
}
}
`,
})
export class Loader {}