From fa144db010fbfe47c04dd2bed34e5625ca996e97 Mon Sep 17 00:00:00 2001 From: kushal-saha Date: Fri, 24 Apr 2026 09:34:18 +0000 Subject: [PATCH] add ui --- frontend/package-lock.json | 132 ++------- frontend/package.json | 3 +- frontend/src/app/app.config.ts | 4 +- frontend/src/app/app.html | 343 ---------------------- frontend/src/app/app.routes.ts | 7 +- frontend/src/app/chat/chat.component.css | 65 ++++ frontend/src/app/chat/chat.component.html | 63 ++++ frontend/src/app/chat/chat.component.ts | 55 ++++ frontend/src/app/chat/chat.store.ts | 98 +++++++ 9 files changed, 310 insertions(+), 460 deletions(-) create mode 100644 frontend/src/app/chat/chat.component.css create mode 100644 frontend/src/app/chat/chat.component.html create mode 100644 frontend/src/app/chat/chat.component.ts create mode 100644 frontend/src/app/chat/chat.store.ts diff --git a/frontend/package-lock.json b/frontend/package-lock.json index f649c40..32dffee 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -14,6 +14,7 @@ "@angular/forms": "^21.2.0", "@angular/platform-browser": "^21.2.0", "@angular/router": "^21.2.0", + "@ngrx/signals": "^21.1.0", "rxjs": "~7.8.0", "tslib": "^2.3.0" }, @@ -2455,9 +2456,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -2475,9 +2473,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -2495,9 +2490,6 @@ "ppc64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -2515,9 +2507,6 @@ "riscv64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -2535,9 +2524,6 @@ "s390x" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -2555,9 +2541,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -2575,9 +2558,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -2674,6 +2654,23 @@ "@emnapi/runtime": "^1.7.1" } }, + "node_modules/@ngrx/signals": { + "version": "21.1.0", + "resolved": "https://registry.npmjs.org/@ngrx/signals/-/signals-21.1.0.tgz", + "integrity": "sha512-eDGaBs6sssEZe2cLGwDdRnfTwyx9TKD+zpLW8yDov6acWzyi0nW5qxUWjpMRwmIU1i+o0t5MMpQaF/rFMmtHSg==", + "dependencies": { + "tslib": "^2.3.0" + }, + "peerDependencies": { + "@angular/core": "^21.0.0", + "rxjs": "^6.5.3 || ^7.4.0" + }, + "peerDependenciesMeta": { + "rxjs": { + "optional": true + } + } + }, "node_modules/@npmcli/agent": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/@npmcli/agent/-/agent-4.0.0.tgz", @@ -3021,9 +3018,6 @@ "arm" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -3045,9 +3039,6 @@ "arm" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -3069,9 +3060,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -3093,9 +3081,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -3117,9 +3102,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -3141,9 +3123,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -3321,9 +3300,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -3341,9 +3317,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -3361,9 +3334,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -3381,9 +3351,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -3560,9 +3527,6 @@ "arm" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -3577,9 +3541,6 @@ "arm" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -3594,9 +3555,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -3611,9 +3569,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -3628,9 +3583,6 @@ "loong64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -3645,9 +3597,6 @@ "loong64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -3662,9 +3611,6 @@ "ppc64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -3679,9 +3625,6 @@ "ppc64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -3696,9 +3639,6 @@ "riscv64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -3713,9 +3653,6 @@ "riscv64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -3730,9 +3667,6 @@ "s390x" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -3747,9 +3681,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -3764,9 +3695,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -4093,9 +4021,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -4113,9 +4038,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -4133,9 +4055,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -4153,9 +4072,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -6485,9 +6401,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -6509,9 +6422,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -6533,9 +6443,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -6557,9 +6464,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MPL-2.0", "optional": true, "os": [ diff --git a/frontend/package.json b/frontend/package.json index 31a3c88..8f6ebd4 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -17,6 +17,7 @@ "@angular/forms": "^21.2.0", "@angular/platform-browser": "^21.2.0", "@angular/router": "^21.2.0", + "@ngrx/signals": "^21.1.0", "rxjs": "~7.8.0", "tslib": "^2.3.0" }, @@ -32,4 +33,4 @@ "typescript": "~5.9.2", "vitest": "^4.0.8" } -} \ No newline at end of file +} diff --git a/frontend/src/app/app.config.ts b/frontend/src/app/app.config.ts index cb1270e..4451829 100644 --- a/frontend/src/app/app.config.ts +++ b/frontend/src/app/app.config.ts @@ -1,11 +1,13 @@ import { ApplicationConfig, provideBrowserGlobalErrorListeners } from '@angular/core'; import { provideRouter } from '@angular/router'; +import { provideHttpClient } from '@angular/common/http'; import { routes } from './app.routes'; export const appConfig: ApplicationConfig = { providers: [ provideBrowserGlobalErrorListeners(), - provideRouter(routes) + provideRouter(routes), + provideHttpClient() ] }; diff --git a/frontend/src/app/app.html b/frontend/src/app/app.html index a1c4296..67e7bd4 100644 --- a/frontend/src/app/app.html +++ b/frontend/src/app/app.html @@ -1,344 +1 @@ - - - - - - - - - - - -
-
-
- -

Hello, {{ title() }}

-

Congratulations! Your app is running. 🎉

-
- -
-
- @for (item of [ - { title: 'Explore the Docs', link: 'https://angular.dev' }, - { title: 'Learn with Tutorials', link: 'https://angular.dev/tutorials' }, - { title: 'Prompt and best practices for AI', link: 'https://angular.dev/ai/develop-with-ai'}, - { title: 'CLI Docs', link: 'https://angular.dev/tools/cli' }, - { title: 'Angular Language Service', link: 'https://angular.dev/tools/language-service' }, - { title: 'Angular DevTools', link: 'https://angular.dev/tools/devtools' }, - ]; track item.title) { - - {{ item.title }} - - - - - } -
- -
-
-
- - - - - - - - - - diff --git a/frontend/src/app/app.routes.ts b/frontend/src/app/app.routes.ts index dc39edb..2052839 100644 --- a/frontend/src/app/app.routes.ts +++ b/frontend/src/app/app.routes.ts @@ -1,3 +1,8 @@ import { Routes } from '@angular/router'; -export const routes: Routes = []; +export const routes: Routes = [ + { + path: '', + loadComponent: () => import('./chat/chat.component').then(m => m.ChatComponent) + } +]; diff --git a/frontend/src/app/chat/chat.component.css b/frontend/src/app/chat/chat.component.css new file mode 100644 index 0000000..2bc3261 --- /dev/null +++ b/frontend/src/app/chat/chat.component.css @@ -0,0 +1,65 @@ +@import url('https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;500;600&display=swap'); + +:host { + display: block; + height: 100vh; + width: 100vw; + color: white; + overflow: hidden; + margin: 0; + padding: 0; + box-sizing: border-box; + font-family: 'Outfit', sans-serif; + background: radial-gradient(circle at top right, #1a1a2e 0%, #16213e 50%, #0f3460 100%); +} + +.custom-scrollbar::-webkit-scrollbar { + width: 6px; +} + +.custom-scrollbar::-webkit-scrollbar-track { + background: transparent; +} + +.custom-scrollbar::-webkit-scrollbar-thumb { + background: rgba(255, 255, 255, 0.1); + border-radius: 10px; +} + +@keyframes slideUpFade { + from { + opacity: 0; + transform: translateY(40px) scale(0.98); + } + to { + opacity: 1; + transform: translateY(0) scale(1); + } +} + +@keyframes messageAppear { + from { + opacity: 0; + transform: translateY(10px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +@keyframes typing { + 0%, 80%, 100% { transform: scale(0); opacity: 0.3; } + 40% { transform: scale(1); opacity: 1; } +} + +@keyframes pulse { + 0% { box-shadow: 0 0 0 0 rgba(79, 172, 254, 0.4); } + 70% { box-shadow: 0 0 0 10px rgba(79, 172, 254, 0); } + 100% { box-shadow: 0 0 0 0 rgba(79, 172, 254, 0); } +} + +@keyframes fadeIn { + from { opacity: 0; } + to { opacity: 1; } +} diff --git a/frontend/src/app/chat/chat.component.html b/frontend/src/app/chat/chat.component.html new file mode 100644 index 0000000..82d34dd --- /dev/null +++ b/frontend/src/app/chat/chat.component.html @@ -0,0 +1,63 @@ +
+
+
AI
+
+

Post Assistant

+

Always online, ready to write.

+
+
+ +
+ @for (msg of chatStore.messages(); track msg.id) { +
+
+ {{ msg.content }} +
+
+ {{ msg.timestamp | date:'shortTime' }} +
+
+ } + + @if (chatStore.isLoading()) { +
+
+
+
+
+ } +
+ +
+ @if (errorMessage) { +
+ {{ errorMessage }} +
+ } +
+ + +
+
+
diff --git a/frontend/src/app/chat/chat.component.ts b/frontend/src/app/chat/chat.component.ts new file mode 100644 index 0000000..1c71df9 --- /dev/null +++ b/frontend/src/app/chat/chat.component.ts @@ -0,0 +1,55 @@ +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'; + +@Component({ + selector: 'app-chat', + standalone: true, + imports: [CommonModule, ReactiveFormsModule], + templateUrl: './chat.component.html', + styleUrl: './chat.component.css' +}) +export class ChatComponent { + readonly chatStore = inject(ChatStore); + + @ViewChild('scrollContainer') private scrollContainer!: ElementRef; + + messageControl = new FormControl(''); + + constructor() { + // Scroll to bottom when messages change + effect(() => { + // Accessing messages will trigger effect on change + const msgs = this.chatStore.messages(); + const loading = this.chatStore.isLoading(); + + setTimeout(() => { + this.scrollToBottom(); + }, 50); + }); + } + + errorMessage = ''; + + sendMessage() { + const value = this.messageControl.value; + if (value && value.trim() && !this.chatStore.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.messageControl.setValue(''); + } + } + + private scrollToBottom(): void { + try { + const el = this.scrollContainer.nativeElement; + el.scrollTop = el.scrollHeight; + } catch(err) { } + } +} diff --git a/frontend/src/app/chat/chat.store.ts b/frontend/src/app/chat/chat.store.ts new file mode 100644 index 0000000..1860b49 --- /dev/null +++ b/frontend/src/app/chat/chat.store.ts @@ -0,0 +1,98 @@ +import { signalStore, withState, withMethods, patchState } from '@ngrx/signals'; + +export type Message = { + id: string; + role: 'user' | 'ai'; + content: string; + timestamp: Date; +}; + +type ChatState = { + messages: Message[]; + isLoading: boolean; +}; + +const initialState: ChatState = { + messages: [ + { + id: 'welcome', + role: 'ai', + content: "What\'s you want to post today ?", + timestamp: new Date() + } + ], + isLoading: false, +}; + +import { inject } from '@angular/core'; +import { HttpClient, HttpErrorResponse } from '@angular/common/http'; +import { lastValueFrom } from 'rxjs'; + +export const ChatStore = signalStore( + { providedIn: 'root' }, + withState(initialState), + withMethods((store) => { + const http = inject(HttpClient); + + return { + sendMessage: async (content: string) => { + // Add user message + const userMessage: Message = { + id: Date.now().toString(), + role: 'user', + content, + timestamp: new Date(), + }; + + patchState(store, (state) => ({ + messages: [...state.messages, userMessage], + isLoading: true + })); + + try { + const response = await lastValueFrom( + http.post<{post: string, imagePrompt?: string}>('http://localhost:8000/api/social-media/generate', { prompt: content }) + ); + + const aiMessage: Message = { + id: (Date.now() + 1).toString(), + role: 'ai', + content: response.post, + timestamp: new Date(), + }; + + patchState(store, (state) => ({ + messages: [...state.messages, aiMessage], + isLoading: false + })); + } catch (error: any) { + let errorText = 'Sorry, I encountered an error while communicating with the server.'; + + if (error instanceof HttpErrorResponse && error.status === 422) { + errorText = error.error?.message || 'Validation error.'; + + // Extract the first error message from the 'errors' object if available + if (error.error?.errors) { + const firstErrorKey = Object.keys(error.error.errors)[0]; + if (firstErrorKey && error.error.errors[firstErrorKey].length > 0) { + errorText = error.error.errors[firstErrorKey][0]; + } + } + } + + const errorMessage: Message = { + id: (Date.now() + 1).toString(), + role: 'ai', + content: errorText, + timestamp: new Date(), + }; + + patchState(store, (state) => ({ + messages: [...state.messages, errorMessage], + isLoading: false + })); + } + } + }; + }) +);