mirror of
https://github.com/paperless-ngx/paperless-ngx.git
synced 2025-05-27 13:18:18 -05:00
Sweet chat animation, cursor
This commit is contained in:
parent
9c3249f1f1
commit
348d175d42
@ -8,7 +8,10 @@
|
|||||||
<div class="chat-messages font-monospace small">
|
<div class="chat-messages font-monospace small">
|
||||||
@for (message of messages; track message) {
|
@for (message of messages; track message) {
|
||||||
<div class="message d-flex flex-row small" [class.justify-content-end]="message.role === 'user'">
|
<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>
|
||||||
}
|
}
|
||||||
<div #scrollAnchor></div>
|
<div #scrollAnchor></div>
|
||||||
|
@ -20,3 +20,18 @@
|
|||||||
right: -3rem;
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
import { NgClass } from '@angular/common'
|
|
||||||
import { Component, ElementRef, ViewChild } from '@angular/core'
|
import { Component, ElementRef, ViewChild } from '@angular/core'
|
||||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
|
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
|
||||||
import { NgbDropdownModule } from '@ng-bootstrap/ng-bootstrap'
|
import { NgbDropdownModule } from '@ng-bootstrap/ng-bootstrap'
|
||||||
@ -12,7 +11,6 @@ import { ChatMessage, ChatService } from 'src/app/services/chat.service'
|
|||||||
ReactiveFormsModule,
|
ReactiveFormsModule,
|
||||||
NgxBootstrapIconsModule,
|
NgxBootstrapIconsModule,
|
||||||
NgbDropdownModule,
|
NgbDropdownModule,
|
||||||
NgClass,
|
|
||||||
],
|
],
|
||||||
templateUrl: './chat.component.html',
|
templateUrl: './chat.component.html',
|
||||||
styleUrl: './chat.component.scss',
|
styleUrl: './chat.component.scss',
|
||||||
@ -25,6 +23,9 @@ export class ChatComponent {
|
|||||||
@ViewChild('scrollAnchor') scrollAnchor!: ElementRef<HTMLDivElement>
|
@ViewChild('scrollAnchor') scrollAnchor!: ElementRef<HTMLDivElement>
|
||||||
@ViewChild('inputField') inputField!: ElementRef<HTMLInputElement>
|
@ViewChild('inputField') inputField!: ElementRef<HTMLInputElement>
|
||||||
|
|
||||||
|
private typewriterBuffer: string[] = []
|
||||||
|
private typewriterActive = false
|
||||||
|
|
||||||
constructor(private chatService: ChatService) {}
|
constructor(private chatService: ChatService) {}
|
||||||
|
|
||||||
sendMessage(): void {
|
sendMessage(): void {
|
||||||
@ -45,9 +46,9 @@ export class ChatComponent {
|
|||||||
|
|
||||||
this.chatService.streamChat(this.documentId, this.input).subscribe({
|
this.chatService.streamChat(this.documentId, this.input).subscribe({
|
||||||
next: (chunk) => {
|
next: (chunk) => {
|
||||||
assistantMessage.content += chunk.substring(lastPartialLength)
|
const delta = chunk.substring(lastPartialLength)
|
||||||
lastPartialLength = chunk.length
|
lastPartialLength = chunk.length
|
||||||
this.scrollToBottom()
|
this.enqueueTypewriter(delta, assistantMessage)
|
||||||
},
|
},
|
||||||
error: () => {
|
error: () => {
|
||||||
assistantMessage.content += '\n\n⚠️ Error receiving response.'
|
assistantMessage.content += '\n\n⚠️ Error receiving response.'
|
||||||
@ -64,13 +65,37 @@ export class ChatComponent {
|
|||||||
this.input = ''
|
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(() => {
|
setTimeout(() => {
|
||||||
this.scrollAnchor?.nativeElement?.scrollIntoView({ behavior: 'smooth' })
|
this.scrollAnchor?.nativeElement?.scrollIntoView({ behavior: 'smooth' })
|
||||||
}, 50)
|
}, 50)
|
||||||
}
|
}
|
||||||
|
|
||||||
onOpenChange(open: boolean): void {
|
public onOpenChange(open: boolean): void {
|
||||||
if (open) {
|
if (open) {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
this.inputField.nativeElement.focus()
|
this.inputField.nativeElement.focus()
|
||||||
|
Loading…
x
Reference in New Issue
Block a user