diff --git a/frontend/src/app/chat/chat.store.ts b/frontend/src/app/chat/chat.store.ts
index eca5060..f263325 100644
--- a/frontend/src/app/chat/chat.store.ts
+++ b/frontend/src/app/chat/chat.store.ts
@@ -2,12 +2,12 @@ import { patchState, signalStore, withMethods, withState } from '@ngrx/signals';
import { inject } from '@angular/core';
import { HttpErrorResponse } from '@angular/common/http';
import { ChatService } from './chat-service';
-import { ChatState, Message, MessageResponse } from './chat.types';
+import { MessageState, Message, MessageResponse, ChatResponse, ChatState } from './chat.types';
import { lastValueFrom } from 'rxjs';
import { Router } from '@angular/router';
import { JsonApiResource } from '../core/types/api';
-const initialState: ChatState = {
+const initialMessageState: MessageState = {
messages: [
{
id: 'null',
@@ -24,9 +24,14 @@ const initialState: ChatState = {
id: null,
};
-export const ChatStore = signalStore(
+const initalChatState: ChatState = {
+ chats: [],
+ isLoading: true,
+};
+
+export const MessageStore = signalStore(
{ providedIn: 'root' },
- withState(initialState),
+ withState(initialMessageState),
withMethods((store) => {
const chatService = inject(ChatService);
const router = inject(Router);
@@ -155,3 +160,25 @@ export const ChatStore = signalStore(
};
}),
);
+
+export const ChatStore = signalStore(
+ { providedIn: 'root' },
+ withState(initalChatState),
+ withMethods((store) => {
+ const chatService = inject(ChatService);
+ return {
+ fetchChats: async () => {
+ patchState(store, { isLoading: true });
+ try {
+ const chats = await lastValueFrom(chatService.getChats());
+ patchState(store, {
+ isLoading: false,
+ chats: chats.data,
+ });
+ } catch (error: any) {
+ patchState(store, { isLoading: false });
+ }
+ },
+ };
+ }),
+);
diff --git a/frontend/src/app/chat/chat.ts b/frontend/src/app/chat/chat.ts
index 534f41a..2cf11c0 100644
--- a/frontend/src/app/chat/chat.ts
+++ b/frontend/src/app/chat/chat.ts
@@ -1,7 +1,7 @@
import { Component, ElementRef, ViewChild, effect, inject } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormControl, ReactiveFormsModule } from '@angular/forms';
-import { ChatStore } from './chat.store';
+import { ChatStore, MessageStore } from './chat.store';
import { ActivatedRoute } from '@angular/router';
@Component({
@@ -12,8 +12,8 @@ import { ActivatedRoute } from '@angular/router';
styleUrl: './chat.css',
})
export class Chat {
- readonly chatStore = inject(ChatStore);
- private route = inject(ActivatedRoute);
+ protected readonly messageStore = inject(MessageStore);
+ private readonly route = inject(ActivatedRoute);
@ViewChild('scrollContainer') private scrollContainer!: ElementRef;
@@ -23,8 +23,8 @@ export class Chat {
// Scroll to bottom when messages change
effect(() => {
// Accessing messages will trigger effect on change
- const msgs = this.chatStore.messages();
- const loading = this.chatStore.isLoading();
+ const msgs = this.messageStore.messages();
+ const loading = this.messageStore.isLoading();
setTimeout(() => {
this.scrollToBottom();
@@ -37,8 +37,8 @@ export class Chat {
const urlId = params.get('id');
if (urlId) {
// If an ID exists in the URL, populate it in the store
- this.chatStore.setChatId(urlId);
- this.chatStore.fetchChatHistory();
+ this.messageStore.setChatId(urlId);
+ this.messageStore.fetchChatHistory();
}
});
}
@@ -47,14 +47,14 @@ export class Chat {
sendMessage() {
const value = this.messageControl.value;
- if (value && value.trim() && !this.chatStore.isLoading()) {
+ if (value && value.trim() && !this.messageStore.isLoading()) {
const words = value.trim().split(/\s+/).length;
if (words > 400) {
this.errorMessage = `Input must be under 400 words (currently ${words} words).`;
return;
}
this.errorMessage = '';
- this.chatStore.sendMessage(value.trim());
+ this.messageStore.sendMessage(value.trim());
this.messageControl.setValue('');
}
}
diff --git a/frontend/src/app/chat/chat.types.ts b/frontend/src/app/chat/chat.types.ts
index 7faa03f..952e8f3 100644
--- a/frontend/src/app/chat/chat.types.ts
+++ b/frontend/src/app/chat/chat.types.ts
@@ -1,13 +1,14 @@
import { JsonApiCollection, JsonApiDocument, JsonApiResource } from '../core/types/api';
-export interface NewChatAttributes {
+export interface Chat {
id: string;
title: string | null;
- createdAt: string;
- updatedAt: string;
+ createdAt: Date;
+ updatedAt: Date;
}
-export type NewChatResponse = JsonApiDocument
;
+export type ChatResponse = JsonApiDocument;
+export type ChatCollection = JsonApiCollection;
export interface Message {
role: 'assistant' | 'user';
@@ -19,8 +20,12 @@ export interface Message {
export type MessageResponse = JsonApiDocument;
export type MessageCollection = JsonApiCollection;
-export type ChatState = {
+export type MessageState = {
messages: JsonApiResource[];
isLoading: boolean;
id: string | null;
};
+export type ChatState = {
+ chats: JsonApiResource[];
+ isLoading: boolean;
+};
diff --git a/frontend/src/app/core/layout/header/header.ts b/frontend/src/app/core/layout/header/header.ts
index 557d802..15d1cd5 100644
--- a/frontend/src/app/core/layout/header/header.ts
+++ b/frontend/src/app/core/layout/header/header.ts
@@ -1,7 +1,7 @@
import { Component, inject } from '@angular/core';
import { RouterLink } from '@angular/router';
import { AuthStore } from '../../../auth/auth.store';
-import { ChatStore } from '../../../chat/chat.store';
+import { MessageStore } from '../../../chat/chat.store';
@Component({
selector: 'app-header',
@@ -11,5 +11,5 @@ import { ChatStore } from '../../../chat/chat.store';
})
export class Header {
protected readonly authStore = inject(AuthStore);
- protected readonly chatStore = inject(ChatStore);
+ protected readonly chatStore = inject(MessageStore);
}
diff --git a/frontend/src/app/core/layout/sidebar/sidebar.css b/frontend/src/app/core/layout/sidebar/sidebar.css
new file mode 100644
index 0000000..0758929
--- /dev/null
+++ b/frontend/src/app/core/layout/sidebar/sidebar.css
@@ -0,0 +1,20 @@
+:host {
+ display: contents;
+}
+
+.scrollbar-thin::-webkit-scrollbar {
+ width: 3px;
+}
+
+.scrollbar-thin::-webkit-scrollbar-track {
+ background: transparent;
+}
+
+.scrollbar-thin::-webkit-scrollbar-thumb {
+ background: #1e2f4d;
+ border-radius: 10px;
+}
+
+.scrollbar-thin::-webkit-scrollbar-thumb:hover {
+ background: #2a3f60;
+}
diff --git a/frontend/src/app/core/layout/sidebar/sidebar.html b/frontend/src/app/core/layout/sidebar/sidebar.html
new file mode 100644
index 0000000..131bbcb
--- /dev/null
+++ b/frontend/src/app/core/layout/sidebar/sidebar.html
@@ -0,0 +1,230 @@
+
+
+
+
+
+
diff --git a/frontend/src/app/core/layout/sidebar/sidebar.spec.ts b/frontend/src/app/core/layout/sidebar/sidebar.spec.ts
new file mode 100644
index 0000000..2f291a9
--- /dev/null
+++ b/frontend/src/app/core/layout/sidebar/sidebar.spec.ts
@@ -0,0 +1,22 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { Sidebar } from './sidebar';
+
+describe('Sidebar', () => {
+ let component: Sidebar;
+ let fixture: ComponentFixture;
+
+ beforeEach(async () => {
+ await TestBed.configureTestingModule({
+ imports: [Sidebar],
+ }).compileComponents();
+
+ fixture = TestBed.createComponent(Sidebar);
+ component = fixture.componentInstance;
+ await fixture.whenStable();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/frontend/src/app/core/layout/sidebar/sidebar.ts b/frontend/src/app/core/layout/sidebar/sidebar.ts
new file mode 100644
index 0000000..ec51625
--- /dev/null
+++ b/frontend/src/app/core/layout/sidebar/sidebar.ts
@@ -0,0 +1,78 @@
+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';
+
+@Component({
+ selector: 'app-sidebar',
+ standalone: true,
+ imports: [CommonModule, RouterModule],
+ templateUrl: 'sidebar.html',
+ styleUrl: 'sidebar.css',
+})
+export class Sidebar implements OnInit {
+ protected chatStore = inject(ChatStore);
+ protected isOpen = signal(true);
+ protected searchQuery = signal('');
+
+ ngOnInit() {
+ this.chatStore.fetchChats();
+ console.log(this.chatStore.chats());
+ }
+
+ protected sidebarClasses = computed(() => {
+ const base =
+ 'fixed top-0 right-0 h-full w-64 border-l border-[#1e2f4d] flex flex-col z-40 transition-transform duration-300 ease-in-out shadow-2xl shadow-black/40 relative overflow-hidden';
+ return this.isOpen() ? base : base + ' translate-x-full';
+ });
+
+ protected chatItemClasses(chat: any): string {
+ const base =
+ 'relative w-full flex items-center px-2 py-2 rounded-lg transition-all duration-150 cursor-pointer group';
+ return chat.isActive
+ ? base + ' bg-[#13213d] text-white'
+ : base + ' hover:bg-[#111a2e] text-[#6a8faf]';
+ }
+
+ protected chatIconClasses(chat: any): string {
+ const base = 'w-6 h-6 rounded-md flex items-center justify-center shrink-0 mt-0.5';
+ return chat.isActive
+ ? base + ' bg-[#2d5be3]/20 text-[#5b8af0]'
+ : base + ' bg-[#111a2e] text-[#3a5272] group-hover:bg-[#1a2a48] group-hover:text-[#5a80a8]';
+ }
+
+ protected toggleSidebar(): void {
+ this.isOpen.update((v) => !v);
+ }
+
+ protected newChat(): void {
+ console.log('New chat triggered');
+ }
+
+ protected selectChat(chat: any): void {
+ // this.activeChatId.set(chat.id);
+ // this.sections.update((sections) =>
+ // sections.map((section) => ({
+ // ...section,
+ // chats: section.chats.map((c) => ({ ...c, isActive: c.id === chat.id })),
+ // })),
+ // );
+ }
+
+ protected onSearch(event: Event): void {
+ this.searchQuery.set((event.target as HTMLInputElement).value);
+ }
+
+ protected formatTime(dateValue: string | Date): string {
+ const date = new Date(dateValue);
+ const now = new Date();
+ const diff = now.getTime() - date.getTime();
+ const mins = Math.floor(diff / 60000);
+ const hours = Math.floor(mins / 60);
+
+ if (mins < 1) return 'now';
+ if (mins < 60) return `${mins}m`;
+ if (hours < 24) return `${hours}h`;
+ return `${Math.floor(hours / 24)}d`;
+ }
+}
diff --git a/frontend/src/index.html b/frontend/src/index.html
index 06cfd45..ec0b4ee 100644
--- a/frontend/src/index.html
+++ b/frontend/src/index.html
@@ -8,6 +8,6 @@
-
+