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()