mirror of
				https://github.com/paperless-ngx/paperless-ngx.git
				synced 2025-10-31 10:37:12 -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"> |       <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