Files
AIEC-new/AIEC-server/js/stream-status.js

468 lines
17 KiB
JavaScript
Raw Normal View History

2025-10-17 09:31:28 +08:00
/**
* 流式状态显示组件
* 用于显示RAG检索过程的实时状态
*/
class StreamStatusDisplay {
constructor(container) {
this.container = container;
this.statusElements = {};
this.retrievalHistory = []; // 记录检索历史
this.statusHistory = []; // 记录所有状态历史
this.maxDisplayItems = 3; // 最多显示3条状态
this.eventQueue = []; // 事件队列
this.isProcessing = false; // 是否正在处理队列
this.minDisplayTime = 500; // 每个状态最少显示500ms
this.init();
}
init() {
// 创建状态显示区域
this.statusArea = document.createElement('div');
this.statusArea.className = 'stream-status-area';
this.statusArea.style.cssText = `
padding: 12px 16px;
background: linear-gradient(135deg, #f6f9fc 0%, #f0f4f8 100%);
border-radius: 12px;
margin-bottom: 16px;
font-size: 14px;
color: #475569;
min-height: 48px;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
display: flex;
align-items: center;
gap: 12px;
position: relative;
overflow: hidden;
`;
// 添加动画背景
const animBg = document.createElement('div');
animBg.style.cssText = `
position: absolute;
top: 0;
left: -100%;
width: 100%;
height: 100%;
background: linear-gradient(90deg, transparent, rgba(99, 102, 241, 0.1), transparent);
animation: shimmer 2s infinite;
`;
this.statusArea.appendChild(animBg);
// 添加CSS动画
if (!document.getElementById('stream-status-styles')) {
const style = document.createElement('style');
style.id = 'stream-status-styles';
style.textContent = `
@keyframes shimmer {
0% { left: -100%; }
100% { left: 100%; }
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.5; }
}
@keyframes rotate {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
.status-icon {
font-size: 20px;
animation: pulse 1.5s ease-in-out infinite;
}
.status-spinner {
animation: rotate 1s linear infinite;
}
`;
document.head.appendChild(style);
}
// 状态内容容器
this.contentArea = document.createElement('div');
this.contentArea.style.cssText = `
position: relative;
z-index: 1;
flex: 1;
display: flex;
align-items: center;
gap: 12px;
`;
this.statusArea.appendChild(this.contentArea);
this.container.appendChild(this.statusArea);
}
updateStatus(event) {
// 清除淡出定时器
if (this.fadeTimeout) {
clearTimeout(this.fadeTimeout);
}
switch (event.type) {
case 'cache_hit':
case 'cache':
if (event.cached) {
this.showCacheHit();
} else {
this.showCacheMiss();
}
break;
case 'cache_miss':
this.showCacheMiss();
break;
case 'starting':
this.showStatus('🔍 开始分析查询...', '#6366f1');
break;
case 'complexity_check':
const complexity = event.data;
if (complexity) {
if (complexity.is_complex) {
this.showStatus(
`📊 复杂查询 (${complexity.level || '高'}) - 置信度: ${(complexity.confidence * 100).toFixed(1)}%`,
'#f59e0b'
);
} else {
this.showStatus(
`📊 简单查询 - 置信度: ${(complexity.confidence * 100).toFixed(1)}%`,
'#10b981'
);
}
}
break;
case 'documents':
const docs = event.data;
if (docs) {
// 记录检索历史
this.retrievalHistory.push({
count: docs.count,
new_docs: docs.new_docs || 0,
is_incremental: docs.is_incremental || false,
retrieval_type: docs.retrieval_type || '检索',
retrieval_reason: docs.retrieval_reason || ''
});
// 根据是否是增量检索显示不同信息
let message = '';
if (docs.is_incremental) {
// 增量检索(第二次及以后)
const reason = docs.retrieval_reason || '继续检索';
message = `📚 ${reason}:新增 ${docs.new_docs || 0} 篇文档(总计 ${docs.count} 篇)`;
this.showStatus(message, '#8b5cf6'); // 紫色表示增量
} else {
// 初始检索
message = `📚 已检索 ${docs.count} 篇文档`;
if (docs.retrieval_type) {
message = `📚 ${docs.retrieval_type}${docs.count} 篇文档`;
}
this.showStatus(message, '#6366f1'); // 蓝色表示初始
}
// 显示检索历史汇总
if (this.retrievalHistory.length > 1) {
this.showRetrievalSummary();
}
// 显示来源文档
if (docs.sources && docs.sources.length > 0) {
this.addSources(docs.sources);
}
}
break;
case 'sufficiency_check':
const sufficient = event.data;
if (sufficient) {
// 置信度可能是数字或未定义
const confidence = sufficient.confidence !== undefined ? sufficient.confidence : 0.5;
if (sufficient.is_sufficient) {
this.showStatus(
`✅ 信息充分 (置信度: ${(confidence * 100).toFixed(1)}%)`,
'#10b981'
);
} else {
this.showStatus(
`⚠️ 信息不足,继续检索... (置信度: ${(confidence * 100).toFixed(1)}%)`,
'#f59e0b'
);
}
}
break;
case 'sub_queries':
const queries = event.data;
if (queries && queries.length > 0) {
this.showStatus(`🔄 生成了 ${queries.length} 个子查询`, '#8b5cf6');
this.showSubQueries(queries);
}
break;
case 'iteration':
const iter = event.data;
if (iter) {
// 只显示当前轮次,不显示最大轮次
this.showStatus(`🔄 第 ${iter.current} 轮迭代`, '#6366f1');
}
break;
case 'generating':
// 不要在这里调用scheduleFadeOut
this.showStatus('✨ 正在生成答案...', '#f59e0b'); // 使用黄色表示进行中
break;
case 'cached':
this.showStatus('💾 结果已缓存', '#10b981');
this.scheduleFadeOut();
break;
case 'complete':
this.showStatus('✅ 答案生成完成', '#10b981');
// 答案生成完成后2秒后自动消失
setTimeout(() => {
this.fadeOutAndHide();
}, 2000);
break;
case 'cancelled':
// 清除之前的所有状态
this.statusHistory = [];
// 显示取消状态
this.showStatus('⚠️ 任务已停止', '#ef4444');
// 1.5秒后淡出
setTimeout(() => {
this.fadeOutAndHide();
}, 1500);
break;
default:
if (event.message) {
this.showStatus(event.message, '#6366f1');
}
}
}
showStatus(message, color = '#6366f1') {
// 添加到历史记录
this.statusHistory.push({
message: message,
color: color,
timestamp: Date.now()
});
// 使用独立的渲染方法
this.renderStatuses();
}
showCacheHit() {
this.statusArea.style.background = 'linear-gradient(135deg, #dcfce7 0%, #d1fae5 100%)';
this.statusArea.style.borderLeft = '3px solid #10b981';
this.contentArea.innerHTML = `
<div class="status-icon" style="color: #10b981;">📦</div>
<div class="status-message" style="color: #065f46; font-weight: 500;">
使用缓存结果伪流式输出
</div>
`;
}
showCacheMiss() {
this.statusArea.style.background = 'linear-gradient(135deg, #fef3c7 0%, #fde68a 100%)';
this.statusArea.style.borderLeft = '3px solid #f59e0b';
this.contentArea.innerHTML = `
<div class="status-icon status-spinner" style="color: #f59e0b;">🔍</div>
<div class="status-message" style="color: #78350f; font-weight: 500;">
开始实时检索...
</div>
`;
}
showSubQueries(queries) {
const maxDisplay = 3;
const displayQueries = queries.slice(0, maxDisplay);
const remaining = queries.length - maxDisplay;
// 构建子查询列表HTML
const queriesList = displayQueries.map((q, i) =>
`${i + 1}. ${q.length > 30 ? q.substring(0, 30) + '...' : q}`
).join('<br>');
const moreText = remaining > 0 ? `<br>... 还有 ${remaining}` : '';
// 更新最后一个状态的消息,添加子查询列表
if (this.statusHistory.length > 0) {
const lastStatus = this.statusHistory[this.statusHistory.length - 1];
// 如果消息中还没有子查询列表,就添加
if (!lastStatus.message.includes('<br>')) {
lastStatus.message += `<br><span style="color: #4f46e5; font-size: 12px; margin-left: 32px;">${queriesList}${moreText}</span>`;
// 重新渲染
this.renderStatuses();
}
}
}
showRetrievalSummary() {
// 显示检索历史汇总
const totalRounds = this.retrievalHistory.length;
const totalDocs = this.retrievalHistory[this.retrievalHistory.length - 1].count;
// 创建一个小的汇总显示
const summaryHtml = `
<span style="
display: inline-block;
margin-left: 12px;
padding: 2px 8px;
background: rgba(99, 102, 241, 0.1);
border-radius: 4px;
font-size: 12px;
color: #6366f1;
"> ${totalRounds} 轮检索</span>
`;
// 添加到当前状态显示中
if (!this.contentArea.querySelector('.retrieval-summary')) {
const summarySpan = document.createElement('span');
summarySpan.className = 'retrieval-summary';
summarySpan.innerHTML = summaryHtml;
this.contentArea.appendChild(summarySpan);
} else {
this.contentArea.querySelector('.retrieval-summary').innerHTML = summaryHtml;
}
}
addSources(sources) {
// 不单独添加,而是将来源信息合并到最新的状态消息中
const maxDisplay = 3;
const displaySources = sources.slice(0, maxDisplay);
const sourceText = displaySources.join(', ');
const moreText = sources.length > maxDisplay ? `${sources.length}` : '';
// 更新最后一个状态的消息,添加来源信息
if (this.statusHistory.length > 0) {
const lastStatus = this.statusHistory[this.statusHistory.length - 1];
// 如果消息中还没有来源信息,就添加
if (!lastStatus.message.includes('来源:')) {
lastStatus.message += `<br><span style="color: #64748b; font-size: 12px; margin-left: 32px;">来源: ${sourceText}${moreText}</span>`;
// 重新渲染
this.renderStatuses();
}
}
}
renderStatuses() {
// 抽取渲染逻辑为独立方法
const recentStatuses = this.statusHistory.slice(-this.maxDisplayItems);
this.contentArea.innerHTML = `
<div style="display: flex; flex-direction: column; gap: 8px;">
${recentStatuses.map((status, index) => {
const isLatest = index === recentStatuses.length - 1;
const opacity = isLatest ? 1 : 0.6;
return `
<div style="
display: flex;
align-items: center;
gap: 12px;
opacity: ${opacity};
padding: ${isLatest ? '8px 0' : '4px 0'};
border-bottom: ${!isLatest ? '1px solid rgba(226, 232, 240, 0.5)' : 'none'};
">
<div class="status-icon" style="color: ${status.color}; font-size: ${isLatest ? '20px' : '16px'};"></div>
<div class="status-message" style="
color: #334155;
font-weight: ${isLatest ? '500' : '400'};
font-size: ${isLatest ? '14px' : '12px'};
">
${status.message}
</div>
</div>
`;
}).join('')}
</div>
`;
// 更新背景色
if (recentStatuses.length > 0) {
const latestStatus = recentStatuses[recentStatuses.length - 1];
this.statusArea.style.background = `linear-gradient(135deg, ${this.hexToRgba(latestStatus.color, 0.05)} 0%, ${this.hexToRgba(latestStatus.color, 0.1)} 100%)`;
this.statusArea.style.borderLeft = `3px solid ${latestStatus.color}`;
}
}
scheduleFadeOut() {
// 清除之前的定时器
if (this.fadeTimeout) {
clearTimeout(this.fadeTimeout);
}
// 5秒后淡出
this.fadeTimeout = setTimeout(() => {
this.fadeOut();
}, 5000);
}
fadeOut() {
this.statusArea.style.opacity = '0.3';
this.statusArea.style.transform = 'scale(0.98)';
// 不完全隐藏,保留淡化状态
// setTimeout(() => {
// this.statusArea.style.display = 'none';
// }, 300);
}
fadeOutAndHide() {
// 添加过渡动画
this.statusArea.style.transition = 'all 0.5s ease-out';
this.statusArea.style.opacity = '0';
this.statusArea.style.transform = 'scale(0.95)';
this.statusArea.style.maxHeight = this.statusArea.offsetHeight + 'px';
// 动画结束后完全隐藏
setTimeout(() => {
this.statusArea.style.maxHeight = '0';
this.statusArea.style.padding = '0';
this.statusArea.style.margin = '0';
this.statusArea.style.overflow = 'hidden';
// 再过渡一段时间后完全移除显示
setTimeout(() => {
this.statusArea.style.display = 'none';
}, 500);
}, 500);
}
clear() {
if (this.fadeTimeout) {
clearTimeout(this.fadeTimeout);
}
this.contentArea.innerHTML = '';
this.statusArea.style.opacity = '1';
this.statusArea.style.transform = 'scale(1)';
this.statusArea.style.display = 'flex';
this.statusArea.style.background = 'linear-gradient(135deg, #f6f9fc 0%, #f0f4f8 100%)';
this.statusArea.style.borderLeft = 'none';
// 重置历史记录
this.retrievalHistory = [];
this.statusHistory = [];
}
destroy() {
if (this.fadeTimeout) {
clearTimeout(this.fadeTimeout);
}
if (this.statusArea && this.statusArea.parentNode) {
this.statusArea.parentNode.removeChild(this.statusArea);
}
}
hexToRgba(hex, alpha) {
const r = parseInt(hex.slice(1, 3), 16);
const g = parseInt(hex.slice(3, 5), 16);
const b = parseInt(hex.slice(5, 7), 16);
return `rgba(${r}, ${g}, ${b}, ${alpha})`;
}
}
// 导出
window.StreamStatusDisplay = StreamStatusDisplay;
console.log('[StreamStatusDisplay] 组件已加载');