Files
AIEC-new/AIEC-server/js/stream-status.js
2025-10-17 09:31:28 +08:00

468 lines
17 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 流式状态显示组件
* 用于显示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] 组件已加载');