mirror of
				https://github.com/paperless-ngx/paperless-ngx.git
				synced 2025-10-31 02:27:10 -04:00 
			
		
		
		
	Sweet chat animation, cursor
This commit is contained in:
		
							parent
							
								
									d99f2d6160
								
							
						
					
					
						commit
						dd1da9f072
					
				| @ -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> | ||||
|  | ||||
| @ -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; | ||||
|   } | ||||
| } | ||||
|  | ||||
| @ -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() | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user