chore: add loader, fix chat states
This commit is contained in:
parent
9853c6128c
commit
fad7658ead
@ -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);
|
||||
|
||||
@ -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">
|
||||
|
||||
@ -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.';
|
||||
|
||||
|
||||
@ -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',
|
||||
})
|
||||
|
||||
@ -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 = {
|
||||
|
||||
@ -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 {
|
||||
|
||||
22
frontend/src/app/shared/loader/loader.spec.ts
Normal file
22
frontend/src/app/shared/loader/loader.spec.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
29
frontend/src/app/shared/loader/loader.ts
Normal file
29
frontend/src/app/shared/loader/loader.ts
Normal 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 {}
|
||||
Loading…
x
Reference in New Issue
Block a user