diff --git a/src-ui/src/app/components/chat/chat/chat.component.html b/src-ui/src/app/components/chat/chat/chat.component.html index 8a12ed471..54753c88f 100644 --- a/src-ui/src/app/components/chat/chat/chat.component.html +++ b/src-ui/src/app/components/chat/chat/chat.component.html @@ -8,7 +8,10 @@
@for (message of messages; track message) {
- {{ message.content }} + + {{ message.content }} + @if (message.isStreaming) { | } +
}
diff --git a/src-ui/src/app/components/chat/chat/chat.component.scss b/src-ui/src/app/components/chat/chat/chat.component.scss index 9eb9dadee..4b00cce1b 100644 --- a/src-ui/src/app/components/chat/chat/chat.component.scss +++ b/src-ui/src/app/components/chat/chat/chat.component.scss @@ -20,3 +20,18 @@ right: -3rem; } } + +.blinking-cursor { + font-weight: bold; + font-size: 1.2em; + animation: blink 1s step-end infinite; +} + +@keyframes blink { + from, to { + opacity: 0; + } + 50% { + opacity: 1; + } +} diff --git a/src-ui/src/app/components/chat/chat/chat.component.ts b/src-ui/src/app/components/chat/chat/chat.component.ts index 0d17f132e..750edd937 100644 --- a/src-ui/src/app/components/chat/chat/chat.component.ts +++ b/src-ui/src/app/components/chat/chat/chat.component.ts @@ -1,4 +1,3 @@ -import { NgClass } from '@angular/common' import { Component, ElementRef, ViewChild } from '@angular/core' import { FormsModule, ReactiveFormsModule } from '@angular/forms' import { NgbDropdownModule } from '@ng-bootstrap/ng-bootstrap' @@ -12,7 +11,6 @@ import { ChatMessage, ChatService } from 'src/app/services/chat.service' ReactiveFormsModule, NgxBootstrapIconsModule, NgbDropdownModule, - NgClass, ], templateUrl: './chat.component.html', styleUrl: './chat.component.scss', @@ -25,6 +23,9 @@ export class ChatComponent { @ViewChild('scrollAnchor') scrollAnchor!: ElementRef @ViewChild('inputField') inputField!: ElementRef + private typewriterBuffer: string[] = [] + private typewriterActive = false + constructor(private chatService: ChatService) {} sendMessage(): void { @@ -45,9 +46,9 @@ export class ChatComponent { this.chatService.streamChat(this.documentId, this.input).subscribe({ next: (chunk) => { - assistantMessage.content += chunk.substring(lastPartialLength) + const delta = chunk.substring(lastPartialLength) lastPartialLength = chunk.length - this.scrollToBottom() + this.enqueueTypewriter(delta, assistantMessage) }, error: () => { assistantMessage.content += '\n\n⚠️ Error receiving response.' @@ -64,13 +65,37 @@ export class ChatComponent { this.input = '' } - scrollToBottom(): void { + enqueueTypewriter(chunk: string, message: ChatMessage): void { + if (!chunk) return + + this.typewriterBuffer.push(...chunk.split('')) + + if (!this.typewriterActive) { + this.typewriterActive = true + this.playTypewriter(message) + } + } + + playTypewriter(message: ChatMessage): void { + if (this.typewriterBuffer.length === 0) { + this.typewriterActive = false + return + } + + const nextChar = this.typewriterBuffer.shift()! + message.content += nextChar + this.scrollToBottom() + + setTimeout(() => this.playTypewriter(message), 10) // 10ms per character + } + + private scrollToBottom(): void { setTimeout(() => { this.scrollAnchor?.nativeElement?.scrollIntoView({ behavior: 'smooth' }) }, 50) } - onOpenChange(open: boolean): void { + public onOpenChange(open: boolean): void { if (open) { setTimeout(() => { this.inputField.nativeElement.focus()