396 lines
12 KiB
HTML
396 lines
12 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="zh-CN">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>流式输出测试页面</title>
|
||
<style>
|
||
* {
|
||
margin: 0;
|
||
padding: 0;
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
body {
|
||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
|
||
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
|
||
sans-serif;
|
||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||
min-height: 100vh;
|
||
padding: 20px;
|
||
display: flex;
|
||
justify-content: center;
|
||
align-items: center;
|
||
}
|
||
|
||
.container {
|
||
background: white;
|
||
border-radius: 16px;
|
||
padding: 40px;
|
||
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1);
|
||
max-width: 800px;
|
||
width: 100%;
|
||
}
|
||
|
||
h1 {
|
||
text-align: center;
|
||
color: #333;
|
||
margin-bottom: 30px;
|
||
font-size: 2.5rem;
|
||
font-weight: 700;
|
||
}
|
||
|
||
.controls {
|
||
display: flex;
|
||
gap: 15px;
|
||
margin-bottom: 30px;
|
||
justify-content: center;
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
button {
|
||
background: #667eea;
|
||
color: white;
|
||
border: none;
|
||
padding: 12px 24px;
|
||
border-radius: 8px;
|
||
font-size: 16px;
|
||
font-weight: 600;
|
||
cursor: pointer;
|
||
transition: all 0.3s ease;
|
||
}
|
||
|
||
button:hover {
|
||
background: #5a67d8;
|
||
transform: translateY(-2px);
|
||
}
|
||
|
||
button:active {
|
||
transform: translateY(0);
|
||
}
|
||
|
||
button:disabled {
|
||
background: #a0aec0;
|
||
cursor: not-allowed;
|
||
transform: none;
|
||
}
|
||
|
||
.stream-output {
|
||
background: #f7fafc;
|
||
border: 2px solid #e2e8f0;
|
||
border-radius: 12px;
|
||
padding: 24px;
|
||
min-height: 400px;
|
||
max-height: 600px;
|
||
overflow-y: auto;
|
||
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
|
||
font-size: 16px;
|
||
line-height: 1.8;
|
||
color: #2d3748;
|
||
white-space: pre-wrap;
|
||
word-break: break-word;
|
||
}
|
||
|
||
.stream-output::-webkit-scrollbar {
|
||
width: 8px;
|
||
}
|
||
|
||
.stream-output::-webkit-scrollbar-track {
|
||
background: #edf2f7;
|
||
border-radius: 4px;
|
||
}
|
||
|
||
.stream-output::-webkit-scrollbar-thumb {
|
||
background: #cbd5e0;
|
||
border-radius: 4px;
|
||
}
|
||
|
||
.stream-output::-webkit-scrollbar-thumb:hover {
|
||
background: #a0aec0;
|
||
}
|
||
|
||
.stats {
|
||
margin-top: 20px;
|
||
padding: 15px;
|
||
background: #edf2f7;
|
||
border-radius: 8px;
|
||
font-size: 14px;
|
||
color: #4a5568;
|
||
display: flex;
|
||
justify-content: space-between;
|
||
flex-wrap: wrap;
|
||
gap: 10px;
|
||
}
|
||
|
||
.stats-item {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 5px;
|
||
}
|
||
|
||
.loading-dots {
|
||
display: inline-block;
|
||
}
|
||
|
||
.loading-dots::after {
|
||
content: '.';
|
||
animation: dots 1s steps(5, end) infinite;
|
||
}
|
||
|
||
@keyframes dots {
|
||
0%, 20% { content: '.'; }
|
||
40% { content: '..'; }
|
||
60% { content: '...'; }
|
||
80%, 100% { content: '.'; }
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div class="container">
|
||
<h1>✨ 流式输出测试</h1>
|
||
|
||
<div class="controls">
|
||
<button id="startBtn">开始流式输出</button>
|
||
<button id="stopBtn" disabled>停止输出</button>
|
||
<button id="clearBtn">清空内容</button>
|
||
</div>
|
||
|
||
<div id="output" class="stream-output">
|
||
<div style="color: #718096;">点击上方按钮开始测试流式输出...</div>
|
||
</div>
|
||
|
||
<div class="stats">
|
||
<div class="stats-item">
|
||
<span>状态:</span>
|
||
<span id="status" style="font-weight: 600;">就绪</span>
|
||
</div>
|
||
<div class="stats-item">
|
||
<span>字符数:</span>
|
||
<span id="charCount">0</span>
|
||
</div>
|
||
<div class="stats-item">
|
||
<span>耗时:</span>
|
||
<span id="timeElapsed">0.00s</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<script>
|
||
// 页面元素
|
||
const startBtn = document.getElementById('startBtn');
|
||
const stopBtn = document.getElementById('stopBtn');
|
||
const clearBtn = document.getElementById('clearBtn');
|
||
const output = document.getElementById('output');
|
||
const statusEl = document.getElementById('status');
|
||
const charCountEl = document.getElementById('charCount');
|
||
const timeElapsedEl = document.getElementById('timeElapsed');
|
||
|
||
// 状态变量
|
||
let controller = null;
|
||
let startTime = 0;
|
||
let charCount = 0;
|
||
let timer = null;
|
||
|
||
// 模拟大模型输出的文本内容
|
||
const sampleText = `欢迎使用流式输出测试页面!
|
||
|
||
这是一个模拟大语言模型生成内容的演示。
|
||
|
||
流式输出是一种将生成的内容逐字逐句发送给用户的技术,
|
||
它可以显著提升用户体验,让用户感觉内容是实时生成的。
|
||
|
||
在实际应用中,大语言模型会在生成每个token后立即发送给客户端,
|
||
而不需要等待整个响应完成。
|
||
|
||
这种技术特别适合:
|
||
- 长文本生成
|
||
- 对话系统
|
||
- 代码生成
|
||
- 实时翻译
|
||
|
||
流式输出的优势包括:
|
||
1. 更快的感知响应速度
|
||
2. 更好的用户体验
|
||
3. 更低的内存占用
|
||
4. 支持更长的内容生成
|
||
|
||
在这个测试中,我们将模拟每秒生成约50个字符的速度,
|
||
您可以看到文本是如何逐字逐句显示的。
|
||
|
||
您可以随时点击"停止输出"按钮来中断流式传输,
|
||
也可以点击"清空内容"按钮重新开始。
|
||
|
||
让我们继续探索流式输出的更多应用场景...
|
||
|
||
在Web应用中,流式输出通常使用以下技术实现:
|
||
- Server-Sent Events (SSE)
|
||
- WebSockets
|
||
- HTTP/2 Server Push
|
||
|
||
每种技术都有其优缺点,选择合适的技术取决于具体的应用场景。
|
||
|
||
例如,SSE适合单向的服务器到客户端通信,
|
||
而WebSockets适合双向通信。
|
||
|
||
流式输出不仅提升了用户体验,
|
||
也为开发者提供了更多的灵活性和控制能力。
|
||
|
||
感谢您使用这个测试页面!
|
||
|
||
希望您对流式输出技术有了更深入的了解。
|
||
|
||
如果您有任何问题或建议,欢迎随时提出。
|
||
|
||
祝您使用愉快!`;
|
||
|
||
// 更新统计信息
|
||
function updateStats() {
|
||
charCountEl.textContent = charCount;
|
||
|
||
if (startTime > 0) {
|
||
const elapsed = (Date.now() - startTime) / 1000;
|
||
timeElapsedEl.textContent = elapsed.toFixed(2) + 's';
|
||
}
|
||
}
|
||
|
||
// 清空输出
|
||
function clearOutput() {
|
||
output.innerHTML = '<div style="color: #718096;">点击上方按钮开始测试流式输出...</div>';
|
||
charCount = 0;
|
||
updateStats();
|
||
statusEl.textContent = '就绪';
|
||
}
|
||
|
||
// 模拟流式输出
|
||
async function simulateStreaming() {
|
||
startBtn.disabled = true;
|
||
stopBtn.disabled = false;
|
||
clearOutput();
|
||
|
||
output.textContent = '';
|
||
statusEl.innerHTML = '生成中 <span class="loading-dots"></span>';
|
||
|
||
startTime = Date.now();
|
||
charCount = 0;
|
||
|
||
// 创建AbortController用于取消操作
|
||
controller = new AbortController();
|
||
const signal = controller.signal;
|
||
|
||
try {
|
||
// 使用ReadableStream模拟流式输出
|
||
const stream = new ReadableStream({
|
||
async start(controller) {
|
||
let index = 0;
|
||
|
||
while (index < sampleText.length && !signal.aborted) {
|
||
// 随机延迟,模拟真实生成速度
|
||
const delay = Math.random() * 80 + 40; // 40-120ms
|
||
await new Promise(resolve => setTimeout(resolve, delay));
|
||
|
||
// 每次发送1-5个字符
|
||
const chunkSize = Math.floor(Math.random() * 5) + 1;
|
||
const chunk = sampleText.slice(index, index + chunkSize);
|
||
|
||
controller.enqueue(new TextEncoder().encode(chunk));
|
||
index += chunkSize;
|
||
|
||
// 更新统计信息
|
||
charCount += chunk.length;
|
||
updateStats();
|
||
}
|
||
|
||
controller.close();
|
||
},
|
||
cancel() {
|
||
console.log('流式输出已取消');
|
||
}
|
||
});
|
||
|
||
// 读取并显示流内容
|
||
const reader = stream.getReader();
|
||
const decoder = new TextDecoder();
|
||
|
||
while (true) {
|
||
const { done, value } = await reader.read();
|
||
if (done) break;
|
||
|
||
// 追加内容到输出
|
||
output.textContent += decoder.decode(value, { stream: true });
|
||
|
||
// 自动滚动到底部
|
||
output.scrollTop = output.scrollHeight;
|
||
}
|
||
|
||
// 完成
|
||
statusEl.textContent = '完成';
|
||
output.style.borderColor = '#48bb78';
|
||
|
||
// 添加完成标记
|
||
const doneMarker = document.createElement('div');
|
||
doneMarker.style.cssText = `
|
||
margin-top: 20px;
|
||
padding: 10px;
|
||
background: #48bb78;
|
||
color: white;
|
||
border-radius: 6px;
|
||
text-align: center;
|
||
font-weight: 600;
|
||
`;
|
||
doneMarker.textContent = '✅ 流式输出完成!';
|
||
output.appendChild(doneMarker);
|
||
|
||
} catch (error) {
|
||
if (error.name === 'AbortError') {
|
||
statusEl.textContent = '已停止';
|
||
output.style.borderColor = '#ed8936';
|
||
|
||
const stopMarker = document.createElement('div');
|
||
stopMarker.style.cssText = `
|
||
margin-top: 20px;
|
||
padding: 10px;
|
||
background: #ed8936;
|
||
color: white;
|
||
border-radius: 6px;
|
||
text-align: center;
|
||
font-weight: 600;
|
||
`;
|
||
stopMarker.textContent = '⏸️ 流式输出已停止';
|
||
output.appendChild(stopMarker);
|
||
} else {
|
||
statusEl.textContent = '错误';
|
||
output.innerHTML += `\n\n❌ 发生错误: ${error.message}`;
|
||
output.style.borderColor = '#f56565';
|
||
}
|
||
} finally {
|
||
// 重置状态
|
||
startBtn.disabled = false;
|
||
stopBtn.disabled = true;
|
||
controller = null;
|
||
if (timer) {
|
||
clearInterval(timer);
|
||
timer = null;
|
||
}
|
||
}
|
||
}
|
||
|
||
// 停止流式输出
|
||
function stopStreaming() {
|
||
if (controller) {
|
||
controller.abort();
|
||
controller = null;
|
||
}
|
||
}
|
||
|
||
// 事件监听器
|
||
startBtn.addEventListener('click', simulateStreaming);
|
||
stopBtn.addEventListener('click', stopStreaming);
|
||
clearBtn.addEventListener('click', clearOutput);
|
||
|
||
// 页面加载完成
|
||
window.addEventListener('load', () => {
|
||
console.log('流式输出测试页面已加载');
|
||
});
|
||
</script>
|
||
</body>
|
||
</html> |