在众多AI项目中,打字机效果是很常见的,我们来模拟下这个效果。前端技术采用的是angular,其它技术也类似。
tsimport { 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();
}
}
}
tsimport { 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();
}
}
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>



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