diff --git a/backend/routes/api.php b/backend/routes/api.php index 1ebca80..9aacac7 100644 --- a/backend/routes/api.php +++ b/backend/routes/api.php @@ -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); diff --git a/frontend/src/app/chat/chat.html b/frontend/src/app/chat/chat.html index 78f84b6..d4a3bb6 100644 --- a/frontend/src/app/chat/chat.html +++ b/frontend/src/app/chat/chat.html @@ -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()) { +
+ +
+ } @else{ @for (msg of messageStore.messages(); track msg.id) {
- } @if (messageStore.isLoading()) { + } + + @if (messageStore.isAgentThinking()) {
@@ -33,7 +39,7 @@ class="w-2 h-2 bg-[#4facfe] rounded-full animate-[typing_1.4s_infinite_ease-in-out_both] delay-0" >
- } + } }
diff --git a/frontend/src/app/chat/chat.store.ts b/frontend/src/app/chat/chat.store.ts index f263325..9202b64 100644 --- a/frontend/src/app/chat/chat.store.ts +++ b/frontend/src/app/chat/chat.store.ts @@ -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(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.'; diff --git a/frontend/src/app/chat/chat.ts b/frontend/src/app/chat/chat.ts index e8b8d2e..4db7c5b 100644 --- a/frontend/src/app/chat/chat.ts +++ b/frontend/src/app/chat/chat.ts @@ -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', }) diff --git a/frontend/src/app/chat/chat.types.ts b/frontend/src/app/chat/chat.types.ts index 952e8f3..72492af 100644 --- a/frontend/src/app/chat/chat.types.ts +++ b/frontend/src/app/chat/chat.types.ts @@ -23,6 +23,7 @@ export type MessageCollection = JsonApiCollection; export type MessageState = { messages: JsonApiResource[]; isLoading: boolean; + isAgentThinking: boolean; id: string | null; }; export type ChatState = { diff --git a/frontend/src/app/core/layout/sidebar/sidebar.ts b/frontend/src/app/core/layout/sidebar/sidebar.ts index f3b7260..08a5d74 100644 --- a/frontend/src/app/core/layout/sidebar/sidebar.ts +++ b/frontend/src/app/core/layout/sidebar/sidebar.ts @@ -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 { diff --git a/frontend/src/app/shared/loader/loader.spec.ts b/frontend/src/app/shared/loader/loader.spec.ts new file mode 100644 index 0000000..9152c9f --- /dev/null +++ b/frontend/src/app/shared/loader/loader.spec.ts @@ -0,0 +1,22 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { Loader } from './loader'; + +describe('Loader', () => { + let component: Loader; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [Loader], + }).compileComponents(); + + fixture = TestBed.createComponent(Loader); + component = fixture.componentInstance; + await fixture.whenStable(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/frontend/src/app/shared/loader/loader.ts b/frontend/src/app/shared/loader/loader.ts new file mode 100644 index 0000000..2d8c074 --- /dev/null +++ b/frontend/src/app/shared/loader/loader.ts @@ -0,0 +1,29 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'app-loader', + imports: [], + template: `
`, + 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 {}