2026-01-30
其它
0

目录

代码
RagflowComponent.ts
RagflowService.ts
RagFlowComponent.html
效果演示

在众多AI项目中,打字机效果是很常见的,我们来模拟下这个效果。前端技术采用的是angular,其它技术也类似。

代码

RagflowComponent.ts

ts
import { Component, OnInit, ViewChild, ElementRef } from '@angular/core'; import { RagflowService } from '../services/ragflow.service'; import { Message, Document, ChatHistory } from '../models/ragflow.models'; @Component({ selector: 'app-ragflow', templateUrl: './ragflow.component.html', styleUrls: ['./ragflow.component.css'] }) export class RagflowComponent implements OnInit { @ViewChild('messagesContainer') messagesContainer!: ElementRef; // 输入框绑定的查询文本 query: string = ''; // 消息列表 messages: Message[] = []; // 消息对应的文档映射 messageDocuments: Map<string, Document[]> = new Map(); // 所有文档列表(备用) documents: Document[] = []; // 历史记录 histories: ChatHistory[] = []; // 控制加载状态 isLoading = false; // 控制打字机效果 isTyping = false; constructor(private ragflowService: RagflowService) { } ngOnInit(): void { // 初始化示例数据 this.loadSampleData(); } ngAfterViewChecked() { // 自动滚动到底部 this.scrollToBottom(); } private scrollToBottom(): void { if (this.messagesContainer) { try { this.messagesContainer.nativeElement.scrollTop = this.messagesContainer.nativeElement.scrollHeight; } catch (err) { console.log('Scroll to bottom error:', err); } } } loadSampleData(): void { // 示例历史记录 this.histories = [ { id: '1', title: '关于人工智能的讨论', timestamp: new Date(Date.now() - 3600000) }, { id: '2', title: '机器学习算法比较', timestamp: new Date(Date.now() - 86400000) }, { id: '3', title: '自然语言处理技术', timestamp: new Date(Date.now() - 172800000) } ]; // 添加欢迎消息 setTimeout(() => { this.addRagflowMessage("您好!我是RagFlow AI助手,您可以向我提问任何问题,我可以帮您检索相关信息和文档。", []); }, 500); } // 发送消息 sendMessage(): void { if (!this.query.trim()) return; // 添加用户消息 this.addUserMessage(this.query); // 开始加载 this.isLoading = true; this.isTyping = true; // 使用SSE流式API获取响应 this.getSseStreamResponseFromApi(); } // 使用SSE流式API获取响应 private getSseStreamResponseFromApi(): void { // 创建一个新的AI消息,初始内容为空 const aiMessage: Message = { id: Date.now().toString(), content: '', sender: 'ragflow', timestamp: new Date() }; this.messages.push(aiMessage); // 为该消息初始化空文档列表 this.messageDocuments.set(aiMessage.id, []); // 调用SSE流式API - 在生产环境中使用实际的SSE方法 // this.ragflowService.getSseStreamResponse(this.query).subscribe(...) // 使用模拟的SSE流式API - 开发阶段使用 this.ragflowService.getMockSseStreamResponse(this.query).subscribe({ next: (event) => { if (event.type === 'delta') { // 使用优化的打字机效果处理SSE数据片段 this.processDeltaWithTypewriter(aiMessage, event.data); } else if (event.type === 'source_docs') { // 更新此消息的文档 this.messageDocuments.set(aiMessage.id, event.data); } else if (event.type === 'finish') { // 流完成,结束加载状态 this.isLoading = false; this.isTyping = false; this.query = ''; } }, error: (error) => { console.error('Error getting SSE stream response:', error); this.isLoading = false; this.isTyping = false; // 显示错误信息 const errorMessage: Message = { id: Date.now().toString(), content: '抱歉,发生了一个错误,请稍后重试。', sender: 'ragflow', timestamp: new Date() }; this.messages.push(errorMessage); } }); } // 处理SSE数据片段,使用打字机效果 private processDeltaWithTypewriter(message: Message, delta: string) { // 将delta内容逐字符添加到消息中,实现打字机效果 let index = 0; const addChar = () => { if (index < delta.length) { message.content += delta.charAt(index); index++; // 根据字符类型决定延迟 const delay = this.getCharacterDelay(delta.charAt(index - 1)); setTimeout(addChar, delay); } }; addChar(); } // 添加用户消息 addUserMessage(content: string): void { const message: Message = { id: Date.now().toString(), content, sender: 'user', timestamp: new Date() }; this.messages.push(message); } // 添加RagFlow消息(带打字机效果) addRagflowMessage(content: string, documents: Document[]): void { const message: Message = { id: Date.now().toString(), content: '', sender: 'ragflow', timestamp: new Date() }; this.messages.push(message); // 为该消息关联文档 this.messageDocuments.set(message.id, documents); // 使用优化后的打字机效果 this.typeMessage(message, content); } // 优化后的打字机效果实现 private typeMessage(message: Message, fullText: string): void { // 根据字符类型动态调整打字速度 let i = 0; const typeWriter = () => { if (i < fullText.length) { // 获取当前字符 const char = fullText.charAt(i); // 根据字符类型决定延迟时间 let delay = this.getCharacterDelay(char); message.content += char; i++; setTimeout(typeWriter, delay); } }; typeWriter(); } // 根据字符类型获取延迟时间 private getCharacterDelay(char: string): number { // 中文字符稍微慢一点 if (/[\u4e00-\u9fa5]/.test(char)) { return 50 + Math.random() * 30; // 50-80ms } // 标点符号稍微停顿 else if (/[,。;:!?、,.;:!?\s]/.test(char)) { return 60 + Math.random() * 40; // 60-100ms } // 其他字符正常速度 else { return 30 + Math.random() * 20; // 30-50ms } } // 根据消息ID获取相关文档 getDocumentsForMessage(messageId: string): Document[] { const docs = this.messageDocuments.get(messageId); return docs ? docs : []; } // 下载文档 downloadDocument(doc: Document): void { console.log(`Downloading document: ${doc.name}`); // 这里应该是实际的下载逻辑 alert(`正在模拟下载文档: ${doc.name}`); } // 选择历史记录 selectHistory(history: ChatHistory): void { console.log(`Selected history: ${history.title}`); // 在实际实现中,这里会加载选中的对话历史 const docs: Document[] = [ { id: 'hist-doc1', name: `${history.title}.pdf`, size: '1.5 MB', url: '#' } ]; this.addRagflowMessage(`您选择了历史对话: "${history.title}",这是相关的信息摘要...`, docs); } // 键盘事件处理 handleKeyPress(event: KeyboardEvent): void { if (event.key === 'Enter' && !event.shiftKey) { event.preventDefault(); this.sendMessage(); } } }

RagflowService.ts

ts
import { Injectable } from '@angular/core'; import { HttpClient } from '@angular/common/http'; import { Observable, Subject } from 'rxjs'; import { Message, Document } from '../models/ragflow.models'; @Injectable({ providedIn: 'root' }) export class RagflowService { private baseUrl = 'http://localhost:8000/api/chat'; // 后端SSE接口基础路径 constructor(private http: HttpClient) { } /** * 使用SSE获取流式AI回复 * @param query 用户查询内容 * @param topK 返回文档数 * @returns Observable<{ type: string, data: any }> 返回SSE事件流 */ getSseStreamResponse(query: string, topK: number = 5) { const subject = new Subject<{ type: string, data: any }>(); // 实际生产环境中的代码会创建SSE连接 const eventSource = new EventSource(`${this.baseUrl}/sse?query=${encodeURIComponent(query)}&topK=${topK}`); // 使用类型断言处理自定义事件 eventSource.addEventListener('delta', (event: any) => { subject.next({ type: 'delta', data: event.data }); }); eventSource.addEventListener('source_docs', (event: any) => { const docs: any[] = JSON.parse(event.data); // 转换后端返回的文档数据格式为前端需要的格式 const formattedDocs: Document[] = docs.map(doc => ({ id: doc.id || `doc-${Date.now()}`, name: doc.filename || doc.name || `文档${Date.now()}`, size: doc.size || '未知大小', url: doc.url || '#' })); subject.next({ type: 'source_docs', data: formattedDocs }); }); eventSource.addEventListener('finish', (event: any) => { subject.next({ type: 'finish', data: null }); }); eventSource.onerror = (error) => { subject.error(error); eventSource.close(); }; // 监听complete事件关闭连接 subject.subscribe({ complete: () => { eventSource.close(); } }); return subject.asObservable(); } /** * 模拟流式响应(开发阶段使用) * @param query 用户查询内容 * @param topK 返回文档数 * @returns Observable<{ type: string, data: any }> */ getMockSseStreamResponse(query: string, topK: number = 5) { const subject = new Subject<{ type: string, data: any }>(); // 模拟SSE流式响应 setTimeout(() => { subject.next({ type: 'delta', data: '根据我的检索结果,' }); }, 300); setTimeout(() => { subject.next({ type: 'delta', data: '这个问题涉及到多个方面。' }); }, 600); setTimeout(() => { subject.next({ type: 'delta', data: '首先,RagFlow是一个先进的检索增强生成系统,' }); }, 900); setTimeout(() => { subject.next({ type: 'delta', data: '它能够处理复杂的文档查询任务。' }); }, 1200); // 模拟返回相关文档 setTimeout(() => { const docs: Document[] = [ { id: 'doc1', name: `关于"${query.substring(0, 10)}..."的分析.pdf`, size: '2.4 MB', url: '#' }, { id: 'doc2', name: `${query.substring(0, 8)}相关报告.docx`, size: '1.1 MB', url: '#' } ]; subject.next({ type: 'source_docs', data: docs }); }, 1500); // 发送完成信号 setTimeout(() => { subject.next({ type: 'finish', data: null }); subject.complete(); }, 1800); return subject.asObservable(); } }

RagFlowComponent.html

html
<div class="ragflow-container"> <div class="sidebar"> <div class="history-header"> <h3>对话历史</h3> <button class="new-chat-btn">+ 新建对话</button> </div> <div class="history-list"> <div *ngFor="let history of histories" class="history-item" (click)="selectHistory(history)" > <div class="history-title">{{ history.title }}</div> <div class="history-time">{{ history.timestamp | date:'MM-dd HH:mm' }}</div> </div> </div> </div> <div class="chat-container"> <div class="chat-header"> <h2>RagFlow 文档检索</h2> </div> <div class="messages-container" #messagesContainer> <div *ngFor="let message of messages" class="message-row" [class.user-message]="message.sender === 'user'"> <div class="message-avatar"> <span *ngIf="message.sender === 'user'" class="avatar-user">👤</span> <span *ngIf="message.sender === 'ragflow'" class="avatar-ragflow">🤖</span> </div> <div class="message-content"> <div class="message-text">{{ message.content }}</div> <div class="message-time">{{ message.timestamp | date:'HH:mm' }}</div> <!-- 相关文档仅在AI回复时显示 --> <div class="documents-section" *ngIf="message.sender === 'ragflow' && getDocumentsForMessage(message.id).length > 0"> <h4>相关文档</h4> <div class="document-list"> <div *ngFor="let doc of getDocumentsForMessage(message.id)" class="document-item"> <div class="document-info"> <div class="document-name">{{ doc.name }}</div> <div class="document-size">{{ doc.size }}</div> </div> <button class="download-btn" (click)="downloadDocument(doc)">下载</button> </div> </div> </div> </div> </div> <div *ngIf="isTyping" class="message-row"> <div class="message-avatar"> <span class="avatar-ragflow">🤖</span> </div> <div class="message-content"> <div class="typing-indicator"> <span></span> <span></span> <span></span> </div> <!-- 如果正在输入时也有相关文档,则显示 --> <div class="documents-section" *ngIf="documents.length > 0"> <h4>相关文档</h4> <div class="document-list"> <div *ngFor="let doc of documents" class="document-item"> <div class="document-info"> <div class="document-name">{{ doc.name }}</div> <div class="document-size">{{ doc.size }}</div> </div> <button class="download-btn" (click)="downloadDocument(doc)">下载</button> </div> </div> </div> </div> </div> </div> <div class="input-container"> <textarea [(ngModel)]="query" (keydown)="handleKeyPress($event)" placeholder="请输入您的问题..." class="query-input" rows="1" [disabled]="isLoading" ></textarea> <button (click)="sendMessage()" class="send-btn" [disabled]="isLoading || !query.trim()" > <span *ngIf="isLoading">发送中...</span> <span *ngIf="!isLoading">发送</span> </button> </div> </div> </div>

效果演示

打字机效果.gif

如果对你有用的话,可以打赏哦
打赏
ali pay
wechat pay

本文作者:繁星

本文链接:

版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!