Files
AIEC-new/AIEC-RAG/test_stream.html
2025-10-17 09:31:28 +08:00

806 lines
26 KiB
HTML
Raw Permalink 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.

<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>RAG流式接口测试</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Microsoft YaHei', sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
padding: 20px;
}
.container {
max-width: 1200px;
margin: 0 auto;
}
.header {
text-align: center;
color: white;
margin-bottom: 30px;
}
.header h1 {
font-size: 2.5em;
margin-bottom: 10px;
text-shadow: 2px 2px 4px rgba(0,0,0,0.3);
}
.header p {
font-size: 1.2em;
opacity: 0.9;
}
.test-panel {
background: white;
border-radius: 20px;
padding: 30px;
box-shadow: 0 20px 60px rgba(0,0,0,0.3);
}
.input-section {
margin-bottom: 30px;
}
.input-group {
display: flex;
gap: 15px;
margin-bottom: 15px;
}
input[type="text"] {
flex: 1;
padding: 15px 20px;
border: 2px solid #e2e8f0;
border-radius: 10px;
font-size: 16px;
transition: all 0.3s;
}
input[type="text"]:focus {
outline: none;
border-color: #667eea;
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
}
.button-group {
display: flex;
gap: 10px;
}
button {
padding: 15px 30px;
border: none;
border-radius: 10px;
font-size: 16px;
font-weight: 600;
cursor: pointer;
transition: all 0.3s;
display: flex;
align-items: center;
gap: 8px;
}
.btn-primary {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
}
.btn-primary:hover {
transform: translateY(-2px);
box-shadow: 0 10px 20px rgba(102, 126, 234, 0.3);
}
.btn-primary:disabled {
background: #cbd5e0;
cursor: not-allowed;
transform: none;
}
.btn-secondary {
background: #f7fafc;
color: #4a5568;
border: 2px solid #e2e8f0;
}
.btn-secondary:hover {
background: #edf2f7;
}
.status-section {
margin-bottom: 30px;
padding: 20px;
background: #f7fafc;
border-radius: 10px;
min-height: 400px;
}
.status-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
padding-bottom: 15px;
border-bottom: 2px solid #e2e8f0;
}
.status-title {
font-size: 1.3em;
font-weight: 600;
color: #2d3748;
}
.status-timer {
font-size: 1.1em;
color: #718096;
font-weight: 500;
}
.progress-bar {
height: 8px;
background: #e2e8f0;
border-radius: 10px;
overflow: hidden;
margin-bottom: 20px;
}
.progress-fill {
height: 100%;
background: linear-gradient(90deg, #667eea 0%, #764ba2 100%);
border-radius: 10px;
transition: width 0.3s ease;
position: relative;
}
.progress-fill::after {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: linear-gradient(
90deg,
transparent,
rgba(255, 255, 255, 0.3),
transparent
);
animation: shimmer 2s infinite;
}
@keyframes shimmer {
0% { transform: translateX(-100%); }
100% { transform: translateX(100%); }
}
.status-content {
max-height: 300px;
overflow-y: auto;
padding: 10px;
}
.status-item {
padding: 12px 15px;
margin-bottom: 10px;
background: white;
border-radius: 8px;
border-left: 4px solid #667eea;
animation: slideIn 0.3s ease;
}
@keyframes slideIn {
from {
opacity: 0;
transform: translateX(-20px);
}
to {
opacity: 1;
transform: translateX(0);
}
}
.status-item-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
}
.status-type {
font-weight: 600;
color: #4a5568;
}
.status-time {
font-size: 0.9em;
color: #a0aec0;
}
.status-message {
color: #2d3748;
line-height: 1.5;
}
.status-data {
margin-top: 10px;
padding: 10px;
background: #f7fafc;
border-radius: 6px;
font-size: 0.95em;
}
.sub-queries {
margin-top: 8px;
padding-left: 20px;
}
.sub-queries li {
color: #4a5568;
margin-bottom: 5px;
}
.answer-section {
padding: 20px;
background: #f0fdf4;
border-radius: 10px;
border: 2px solid #86efac;
display: none;
}
.answer-section.show {
display: block;
animation: fadeIn 0.5s ease;
}
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
.answer-header {
font-size: 1.2em;
font-weight: 600;
color: #166534;
margin-bottom: 15px;
}
.answer-content {
color: #2d3748;
line-height: 1.8;
white-space: pre-wrap;
}
.test-options {
display: flex;
gap: 20px;
margin-bottom: 20px;
padding: 15px;
background: #f7fafc;
border-radius: 8px;
}
.option-group {
display: flex;
align-items: center;
gap: 8px;
}
.option-group label {
font-weight: 500;
color: #4a5568;
}
select {
padding: 8px 12px;
border: 2px solid #e2e8f0;
border-radius: 6px;
background: white;
cursor: pointer;
}
.stats {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
gap: 15px;
margin-top: 20px;
}
.stat-card {
padding: 15px;
background: white;
border-radius: 8px;
text-align: center;
}
.stat-value {
font-size: 1.8em;
font-weight: bold;
color: #667eea;
}
.stat-label {
font-size: 0.9em;
color: #718096;
margin-top: 5px;
}
.spinner {
display: inline-block;
width: 20px;
height: 20px;
border: 3px solid rgba(255,255,255,0.3);
border-radius: 50%;
border-top-color: white;
animation: spin 1s ease-in-out infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
.error-message {
padding: 15px;
background: #fee;
border: 1px solid #fcc;
border-radius: 8px;
color: #c00;
margin-bottom: 20px;
display: none;
}
.error-message.show {
display: block;
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>🚀 RAG流式接口测试</h1>
<p>测试Server-Sent Events (SSE)实时状态推送</p>
</div>
<div class="test-panel">
<!-- 测试选项 -->
<div class="test-options">
<div class="option-group">
<label>API地址:</label>
<input type="text" id="apiUrl" value="http://localhost:8080" style="width: 200px;">
</div>
<div class="option-group">
<label>模式:</label>
<select id="mode">
<option value="0">自动判断</option>
<option value="simple">强制简单</option>
<option value="complex">强制复杂</option>
</select>
</div>
</div>
<!-- 输入区域 -->
<div class="input-section">
<div class="input-group">
<input type="text" id="queryInput" placeholder="输入您的问题例如什么是RAG系统" value="什么是RAG系统">
</div>
<div class="button-group">
<button id="streamBtn" class="btn-primary" onclick="testStream()">
<span id="streamBtnText">🔄 测试流式接口</span>
<span id="streamSpinner" class="spinner" style="display: none;"></span>
</button>
<button id="normalBtn" class="btn-secondary" onclick="testNormal()">
📋 测试普通接口
</button>
<button id="stopBtn" class="btn-secondary" onclick="stopStream()" style="display: none;">
⏹️ 停止
</button>
<button class="btn-secondary" onclick="clearStatus()">
🗑️ 清空
</button>
</div>
</div>
<!-- 错误提示 -->
<div id="errorMessage" class="error-message"></div>
<!-- 状态显示区域 -->
<div class="status-section">
<div class="status-header">
<div class="status-title" id="statusTitle">等待测试...</div>
<div class="status-timer" id="statusTimer">⏱️ 0.0秒</div>
</div>
<div class="progress-bar">
<div class="progress-fill" id="progressBar" style="width: 0%"></div>
</div>
<div class="status-content" id="statusContent">
<!-- 状态项将动态添加到这里 -->
</div>
<!-- 统计信息 -->
<div class="stats" id="stats" style="display: none;">
<div class="stat-card">
<div class="stat-value" id="statEvents">0</div>
<div class="stat-label">事件数</div>
</div>
<div class="stat-card">
<div class="stat-value" id="statDocs">0</div>
<div class="stat-label">文档数</div>
</div>
<div class="stat-card">
<div class="stat-value" id="statIterations">0</div>
<div class="stat-label">迭代轮次</div>
</div>
<div class="stat-card">
<div class="stat-value" id="statTime">0</div>
<div class="stat-label">总耗时(秒)</div>
</div>
</div>
</div>
<!-- 答案显示区域 -->
<div id="answerSection" class="answer-section">
<div class="answer-header">💡 最终答案</div>
<div class="answer-content" id="answerContent"></div>
</div>
</div>
</div>
<script>
let eventSource = null;
let startTime = null;
let timerInterval = null;
let eventCount = 0;
let totalDocs = 0;
let iterations = 0;
function testStream() {
const query = document.getElementById('queryInput').value;
const apiUrl = document.getElementById('apiUrl').value;
const mode = document.getElementById('mode').value;
if (!query.trim()) {
showError('请输入查询问题');
return;
}
// 重置状态
clearStatus();
hideError();
// 更新UI
document.getElementById('streamBtn').disabled = true;
document.getElementById('streamBtnText').textContent = '连接中...';
document.getElementById('streamSpinner').style.display = 'inline-block';
document.getElementById('stopBtn').style.display = 'inline-block';
document.getElementById('statusTitle').textContent = '正在连接服务器...';
document.getElementById('stats').style.display = 'grid';
// 开始计时
startTime = Date.now();
timerInterval = setInterval(updateTimer, 100);
// 构建查询参数
const params = new URLSearchParams({
query: query,
mode: mode,
save_output: 'false'
});
// 创建EventSource连接
const url = `${apiUrl}/retrieve/stream`;
// 使用POST请求的EventSource需要polyfill或改用fetch
// 由于标准EventSource不支持POST我们使用fetch + ReadableStream
fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Accept': 'text/event-stream'
},
body: JSON.stringify({
query: query,
mode: mode,
save_output: false
})
})
.then(response => {
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const reader = response.body.getReader();
const decoder = new TextDecoder();
let buffer = '';
function processText(text) {
buffer += text;
const lines = buffer.split('\n');
buffer = lines.pop() || '';
for (const line of lines) {
if (line.startsWith('data: ')) {
const data = line.slice(6);
if (data === '[DONE]') {
onStreamComplete();
return false; // 停止读取
}
try {
const event = JSON.parse(data);
handleEvent(event);
} catch (e) {
console.error('解析错误:', e, data);
}
}
}
return true; // 继续读取
}
function read() {
reader.read().then(({done, value}) => {
if (done) {
onStreamComplete();
return;
}
const text = decoder.decode(value, {stream: true});
const shouldContinue = processText(text);
if (shouldContinue) {
read();
}
}).catch(error => {
console.error('读取错误:', error);
showError('流式读取失败: ' + error.message);
onStreamComplete();
});
}
// 开始读取流
document.getElementById('statusTitle').textContent = '接收数据流...';
document.getElementById('streamBtnText').textContent = '接收中...';
read();
})
.catch(error => {
console.error('连接错误:', error);
showError('连接失败: ' + error.message);
onStreamComplete();
});
}
function handleEvent(event) {
eventCount++;
const progress = event.progress || 0;
document.getElementById('progressBar').style.width = progress + '%';
// 创建状态项
const statusItem = document.createElement('div');
statusItem.className = 'status-item';
const header = document.createElement('div');
header.className = 'status-item-header';
const typeLabel = document.createElement('span');
typeLabel.className = 'status-type';
typeLabel.textContent = getTypeLabel(event.type);
const timeLabel = document.createElement('span');
timeLabel.className = 'status-time';
timeLabel.textContent = new Date().toLocaleTimeString();
header.appendChild(typeLabel);
header.appendChild(timeLabel);
statusItem.appendChild(header);
// 根据类型处理不同的事件
if (event.message) {
const message = document.createElement('div');
message.className = 'status-message';
message.textContent = event.message;
statusItem.appendChild(message);
}
// 处理特定类型的数据
if (event.type === 'sub_queries' && event.formatted_data) {
const list = document.createElement('ol');
list.className = 'sub-queries';
event.formatted_data.forEach(q => {
const li = document.createElement('li');
li.textContent = q;
list.appendChild(li);
});
statusItem.appendChild(list);
} else if (event.type === 'documents' && event.data) {
totalDocs = event.data.count || totalDocs;
document.getElementById('statDocs').textContent = totalDocs;
if (event.formatted_sources) {
const sources = document.createElement('div');
sources.className = 'status-data';
sources.textContent = '来源: ' + event.formatted_sources.join(', ');
statusItem.appendChild(sources);
}
} else if (event.type === 'iteration' && event.data) {
iterations = event.data.current || iterations;
document.getElementById('statIterations').textContent =
`${event.data.current}/${event.data.max}`;
} else if (event.type === 'answer' && event.data) {
displayAnswer(event.data.content);
document.getElementById('statDocs').textContent =
event.data.total_documents || totalDocs;
}
// 添加到状态内容
document.getElementById('statusContent').appendChild(statusItem);
// 更新事件计数
document.getElementById('statEvents').textContent = eventCount;
// 滚动到底部
const container = document.getElementById('statusContent');
container.scrollTop = container.scrollHeight;
}
function getTypeLabel(type) {
const labels = {
'starting': '🚀 开始',
'complexity_check': '🤔 复杂度分析',
'sub_queries': '📝 子查询',
'documents': '📚 文档检索',
'iteration': '🔄 迭代',
'sufficiency_check': '✅ 充分性检查',
'parallel_retrieval': '⚡ 并行检索',
'generating': '✍️ 生成答案',
'answer': '💡 答案',
'error': '❌ 错误'
};
return labels[type] || type;
}
function displayAnswer(content) {
document.getElementById('answerContent').textContent = content;
document.getElementById('answerSection').classList.add('show');
}
function onStreamComplete() {
clearInterval(timerInterval);
document.getElementById('streamBtn').disabled = false;
document.getElementById('streamBtnText').textContent = '🔄 测试流式接口';
document.getElementById('streamSpinner').style.display = 'none';
document.getElementById('stopBtn').style.display = 'none';
document.getElementById('statusTitle').textContent = '✅ 完成';
const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
document.getElementById('statTime').textContent = elapsed;
}
function stopStream() {
if (eventSource) {
eventSource.close();
eventSource = null;
}
onStreamComplete();
document.getElementById('statusTitle').textContent = '⏹️ 已停止';
}
function clearStatus() {
document.getElementById('statusContent').innerHTML = '';
document.getElementById('answerSection').classList.remove('show');
document.getElementById('progressBar').style.width = '0%';
document.getElementById('statusTitle').textContent = '等待测试...';
document.getElementById('statusTimer').textContent = '⏱️ 0.0秒';
document.getElementById('stats').style.display = 'none';
eventCount = 0;
totalDocs = 0;
iterations = 0;
hideError();
}
function updateTimer() {
if (startTime) {
const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
document.getElementById('statusTimer').textContent = `⏱️ ${elapsed}`;
}
}
function testNormal() {
const query = document.getElementById('queryInput').value;
const apiUrl = document.getElementById('apiUrl').value;
const mode = document.getElementById('mode').value;
if (!query.trim()) {
showError('请输入查询问题');
return;
}
clearStatus();
hideError();
document.getElementById('normalBtn').disabled = true;
document.getElementById('statusTitle').textContent = '调用普通接口...';
startTime = Date.now();
timerInterval = setInterval(updateTimer, 100);
fetch(`${apiUrl}/retrieve`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
query: query,
mode: mode,
save_output: false
})
})
.then(response => response.json())
.then(data => {
clearInterval(timerInterval);
document.getElementById('normalBtn').disabled = false;
document.getElementById('statusTitle').textContent = '✅ 普通接口响应';
if (data.answer) {
displayAnswer(data.answer);
}
// 显示响应信息
const statusItem = document.createElement('div');
statusItem.className = 'status-item';
statusItem.innerHTML = `
<div class="status-message">
成功获取响应<br>
答案长度: ${data.answer ? data.answer.length : 0} 字符<br>
文档数: ${data.total_passages || 0}<br>
耗时: ${((Date.now() - startTime) / 1000).toFixed(1)}
</div>
`;
document.getElementById('statusContent').appendChild(statusItem);
})
.catch(error => {
clearInterval(timerInterval);
document.getElementById('normalBtn').disabled = false;
showError('请求失败: ' + error.message);
});
}
function showError(message) {
const errorEl = document.getElementById('errorMessage');
errorEl.textContent = message;
errorEl.classList.add('show');
}
function hideError() {
document.getElementById('errorMessage').classList.remove('show');
}
// 页面加载时的初始化
window.onload = function() {
// 检查服务状态
const apiUrl = document.getElementById('apiUrl').value;
fetch(`${apiUrl}/health`)
.then(response => response.json())
.then(data => {
console.log('服务状态:', data);
})
.catch(error => {
showError('⚠️ 无法连接到服务,请确保后端服务正在运行 (端口8080)');
});
};
</script>
</body>
</html>