Sweet chat animation, cursor

This commit is contained in:
shamoon 2025-04-25 23:08:20 -07:00
parent 9c3249f1f1
commit 348d175d42
No known key found for this signature in database
3 changed files with 50 additions and 7 deletions

View File

@ -8,7 +8,10 @@
<div class="chat-messages font-monospace small">
@for (message of messages; track message) {
<div class="message d-flex flex-row small" [class.justify-content-end]="message.role === 'user'">
<span class="p-2 m-2" [class.bg-dark]="message.role === 'user'">{{ message.content }}</span>
<span class="p-2 m-2" [class.bg-dark]="message.role === 'user'">
{{ message.content }}
@if (message.isStreaming) { <span class="blinking-cursor">|</span> }
</span>
</div>
}
<div #scrollAnchor></div>

View File

@ -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;
}
}

View File

@ -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<HTMLDivElement>
@ViewChild('inputField') inputField!: ElementRef<HTMLInputElement>
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()