From 2374279998d1bb59f943355634aa54f2028dbcc4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Musia=C5=82?= Date: Fri, 9 Feb 2024 12:15:17 +0100 Subject: [PATCH] feat(AI-130): fixes for initial messages and styles improvements (#24) --- .../chat-messages.component.html | 2 +- .../chat-messages/chat-messages.component.ts | 2 +- .../chat-typing/chat-typing.component.html | 2 +- .../chat/chat-typing/chat-typing.component.ts | 2 +- .../components/spinner/spinner.component.html | 1 + .../components/spinner/spinner.component.scss | 44 +++++++++++++++++++ .../spinner/spinner.component.spec.ts | 22 ++++++++++ .../components/spinner/spinner.component.ts | 11 +++++ .../+chat/containers/chat/chat.component.html | 5 ++- .../+chat/containers/chat/chat.component.ts | 5 ++- .../app/modules/+chat/shared/chat.service.ts | 19 +++++--- apps/spa/src/environments/environment.ts | 2 +- .../src/lib/assistant-iframe.styles.ts | 27 +++++++++++- 13 files changed, 130 insertions(+), 14 deletions(-) create mode 100644 apps/spa/src/app/components/spinner/spinner.component.html create mode 100644 apps/spa/src/app/components/spinner/spinner.component.scss create mode 100644 apps/spa/src/app/components/spinner/spinner.component.spec.ts create mode 100644 apps/spa/src/app/components/spinner/spinner.component.ts diff --git a/apps/spa/src/app/components/chat/chat-messages/chat-messages.component.html b/apps/spa/src/app/components/chat/chat-messages/chat-messages.component.html index ea37fed..be2bbba 100644 --- a/apps/spa/src/app/components/chat/chat-messages/chat-messages.component.html +++ b/apps/spa/src/app/components/chat/chat-messages/chat-messages.component.html @@ -6,5 +6,5 @@ } @empty { } - + diff --git a/apps/spa/src/app/components/chat/chat-messages/chat-messages.component.ts b/apps/spa/src/app/components/chat/chat-messages/chat-messages.component.ts index 678a0fb..a60ceb9 100644 --- a/apps/spa/src/app/components/chat/chat-messages/chat-messages.component.ts +++ b/apps/spa/src/app/components/chat/chat-messages/chat-messages.component.ts @@ -30,7 +30,7 @@ import { ChatTipsComponent } from '../chat-tips/chat-tips.component'; }) export class ChatMessagesComponent implements AfterViewInit, OnChanges { @Input() messages: Message[] = []; - @Input() isLoading = false; + @Input() isTyping = false; @Input() tips: string[] = []; @Output() tipSelected$ = new EventEmitter(); @ViewChildren('item') item?: QueryList; diff --git a/apps/spa/src/app/components/chat/chat-typing/chat-typing.component.html b/apps/spa/src/app/components/chat/chat-typing/chat-typing.component.html index 300c3ee..f7ece5d 100644 --- a/apps/spa/src/app/components/chat/chat-typing/chat-typing.component.html +++ b/apps/spa/src/app/components/chat/chat-typing/chat-typing.component.html @@ -1,4 +1,4 @@ -@if (isLoading) { +@if (isTyping) {
diff --git a/apps/spa/src/app/components/chat/chat-typing/chat-typing.component.ts b/apps/spa/src/app/components/chat/chat-typing/chat-typing.component.ts index 632faac..edd7ec8 100644 --- a/apps/spa/src/app/components/chat/chat-typing/chat-typing.component.ts +++ b/apps/spa/src/app/components/chat/chat-typing/chat-typing.component.ts @@ -7,5 +7,5 @@ import { Component, Input } from '@angular/core'; styleUrl: './chat-typing.component.scss', }) export class ChatTypingComponent { - @Input() isLoading = false; + @Input() isTyping = false; } diff --git a/apps/spa/src/app/components/spinner/spinner.component.html b/apps/spa/src/app/components/spinner/spinner.component.html new file mode 100644 index 0000000..966db74 --- /dev/null +++ b/apps/spa/src/app/components/spinner/spinner.component.html @@ -0,0 +1 @@ + diff --git a/apps/spa/src/app/components/spinner/spinner.component.scss b/apps/spa/src/app/components/spinner/spinner.component.scss new file mode 100644 index 0000000..968fa60 --- /dev/null +++ b/apps/spa/src/app/components/spinner/spinner.component.scss @@ -0,0 +1,44 @@ +@import 'settings'; + +:host { + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; + display: flex; + justify-content: center; + align-items: center; + background-color: var(--color-white); + z-index: 2; + opacity: 0; + transition: ease-out opacity 0.3s; + pointer-events: none; + + &.is-active { + opacity: 1; + pointer-events: all; + } +} + +.spinner { + width: 88px; + height: 88px; + border-radius: 50%; + display: inline-block; + position: relative; + border: 10px solid; + border-color: rgba(168, 135, 92, 0.15) rgba(168, 135, 92, 0.25) + rgba(168, 135, 92, 0.35) rgba(168, 135, 92, 0.5); + box-sizing: border-box; + animation: rotation 1s linear infinite; +} + +@keyframes rotation { + 0% { + transform: rotate(0deg); + } + 100% { + transform: rotate(360deg); + } +} diff --git a/apps/spa/src/app/components/spinner/spinner.component.spec.ts b/apps/spa/src/app/components/spinner/spinner.component.spec.ts new file mode 100644 index 0000000..e184823 --- /dev/null +++ b/apps/spa/src/app/components/spinner/spinner.component.spec.ts @@ -0,0 +1,22 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { SpinnerComponent } from './spinner.component'; + +describe('SpinnerComponent', () => { + let component: SpinnerComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [SpinnerComponent], + }).compileComponents(); + + fixture = TestBed.createComponent(SpinnerComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/apps/spa/src/app/components/spinner/spinner.component.ts b/apps/spa/src/app/components/spinner/spinner.component.ts new file mode 100644 index 0000000..b0622d7 --- /dev/null +++ b/apps/spa/src/app/components/spinner/spinner.component.ts @@ -0,0 +1,11 @@ +import { Component, HostBinding, Input } from '@angular/core'; + +@Component({ + selector: 'ai-spinner', + standalone: true, + templateUrl: './spinner.component.html', + styleUrl: './spinner.component.scss', +}) +export class SpinnerComponent { + @HostBinding('class.is-active') @Input() isActive = false; +} diff --git a/apps/spa/src/app/modules/+chat/containers/chat/chat.component.html b/apps/spa/src/app/modules/+chat/containers/chat/chat.component.html index 6dda4b2..fb905bc 100644 --- a/apps/spa/src/app/modules/+chat/containers/chat/chat.component.html +++ b/apps/spa/src/app/modules/+chat/containers/chat/chat.component.html @@ -6,17 +6,18 @@ (close$)="chatService.toggle()" (config$)="chatService.clear()"> + @if (isConfigEnabled && !threadId()) { } @else { (environment.isAutoOpen); isLoading$ = new BehaviorSubject(false); + isVisible$ = new BehaviorSubject(environment.isAutoOpen); + isTyping$ = new BehaviorSubject(false); messages$ = new BehaviorSubject([]); constructor( @@ -63,10 +66,15 @@ export class ChatService { this.threadService.threadId$ .pipe( distinctUntilChanged(), + filter(threadId => !!threadId), + tap(() => this.isLoading$.next(true)), mergeMap(threadId => this.threadService.getThread(threadId)), map((response: ThreadResponse) => this.parseMessages(response)), ) - .subscribe(data => this.messages$.next(data)); + .subscribe(data => { + this.messages$.next(data); + this.isLoading$.next(false); + }); } toggle(): void { @@ -74,6 +82,7 @@ export class ChatService { } refresh(): void { + this.isLoading$.next(true); this.messages$.next([]); this.threadService.start().subscribe(); } @@ -101,7 +110,7 @@ export class ChatService { } async sendMessage(content: string, role = ChatRole.User): Promise { - this.isLoading$.next(true); + this.isTyping$.next(true); this.addMessage({ content, role }); const files = await this.chatFilesService.sendFiles(); @@ -120,12 +129,12 @@ export class ChatService { content: data.content, role: ChatRole.Assistant, }); - this.isLoading$.next(false); + this.isTyping$.next(false); }); } sendAudio(file: Blob): void { - this.isLoading$.next(true); + this.isTyping$.next(true); this.chatClientService .transcription({ diff --git a/apps/spa/src/environments/environment.ts b/apps/spa/src/environments/environment.ts index 0acaa0e..ed9b6f9 100644 --- a/apps/spa/src/environments/environment.ts +++ b/apps/spa/src/environments/environment.ts @@ -3,7 +3,7 @@ export const environment = { appUrl: 'https://ai-assistant-c8b469d88808.herokuapp.com', apiUrl: 'https://ai-assistant-c8b469d88808.herokuapp.com/api', websocketUrl: 'https://ai-assistant-c8b469d88808.herokuapp.com', - isThreadMemorized: false, + isThreadMemorized: true, isAudioEnabled: true, isTranscriptionEnabled: true, isAttachmentEnabled: true, diff --git a/libs/ai-embedded/src/lib/assistant-iframe.styles.ts b/libs/ai-embedded/src/lib/assistant-iframe.styles.ts index 246f8cf..12144b6 100644 --- a/libs/ai-embedded/src/lib/assistant-iframe.styles.ts +++ b/libs/ai-embedded/src/lib/assistant-iframe.styles.ts @@ -14,6 +14,18 @@ export const addIframeClass = (className: string) => `.${className} { border: 0; box-shadow: rgba(17, 17, 26, 0.1) 0 4px 16px, rgba(17, 17, 26, 0.1) 0 8px 32px; z-index: 150; + animation: iframeAnimation 0.2s; +} + +@keyframes iframeAnimation { + 0% { + transform: translateY(20px); + opacity: 0; + } + 100% { + transform: translateY(0); + opacity: 1; + } } @media (max-width: 460px) { @@ -40,6 +52,15 @@ export const addTriggerClass = (className: string) => ` } } +@keyframes triggerFadeIn { + 0% { + opacity: 0; + } + 100% { + opacity: 1; + } +} + .${className} { display: block; position: fixed; @@ -58,6 +79,9 @@ export const addTriggerClass = (className: string) => ` transition: 0.2s all ease-in-out; cursor: pointer; z-index: 100; + opacity: 0; + animation: triggerFadeIn 0.2s ease-in; + animation-delay: 2s; @media (max-width: 460px) { width: 40px; @@ -72,6 +96,7 @@ export const addTriggerClass = (className: string) => ` } .${className}.is-animated { - animation: trigger 1.5s infinite; + animation: trigger 1.5s infinite, triggerFadeIn 1s; + animation-delay: 2s; animation-fill-mode: both; }`;