| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844 |
- <!DOCTYPE html>
- <html lang="zh-CN">
- <head>
- <meta charset="UTF-8">
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
- <title>流式聊天数据库集成测试</title>
- <!-- Vditor CSS -->
- <link rel="stylesheet" href="https://unpkg.com/vditor/dist/index.css" />
- <style>
- * {
- margin: 0;
- padding: 0;
- box-sizing: border-box;
- }
-
- body {
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
- background: #f5f5f5;
- padding: 20px;
- }
-
- .container {
- max-width: 1000px;
- margin: 0 auto;
- background: white;
- border-radius: 8px;
- box-shadow: 0 2px 10px rgba(0,0,0,0.1);
- overflow: hidden;
- }
-
- .header {
- background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
- color: white;
- padding: 30px;
- text-align: center;
- }
-
- .header h1 {
- font-size: 28px;
- margin-bottom: 10px;
- }
-
- .header p {
- opacity: 0.9;
- font-size: 16px;
- }
-
- .content {
- padding: 30px;
- }
-
- .test-section {
- margin-bottom: 30px;
- padding: 25px;
- background: #f8f9fa;
- border-radius: 8px;
- border-left: 4px solid #667eea;
- }
-
- .form-group {
- margin-bottom: 20px;
- }
-
- label {
- display: block;
- margin-bottom: 8px;
- font-weight: 600;
- color: #2c3e50;
- font-size: 14px;
- }
-
- input, textarea {
- width: 100%;
- padding: 12px;
- border: 2px solid #e1e8ed;
- border-radius: 6px;
- font-size: 14px;
- transition: border-color 0.3s;
- }
-
- input:focus, textarea:focus {
- outline: none;
- border-color: #667eea;
- }
-
- textarea {
- height: 100px;
- resize: vertical;
- font-family: inherit;
- }
-
- .button-group {
- display: flex;
- gap: 15px;
- margin-top: 20px;
- }
-
- button {
- padding: 12px 24px;
- border: none;
- border-radius: 6px;
- cursor: pointer;
- font-size: 14px;
- font-weight: 600;
- transition: all 0.3s;
- min-width: 120px;
- }
-
- .btn-primary {
- background: #667eea;
- color: white;
- }
-
- .btn-primary:hover {
- background: #5a6fd8;
- transform: translateY(-1px);
- }
-
- .btn-secondary {
- background: #6c757d;
- color: white;
- }
-
- .btn-secondary:hover {
- background: #5a6268;
- }
-
- .btn-danger {
- background: #dc3545;
- color: white;
- }
-
- .btn-danger:hover {
- background: #c82333;
- }
-
- .status {
- padding: 15px;
- margin: 15px 0;
- border-radius: 6px;
- font-weight: 600;
- display: none;
- }
-
- .status.show {
- display: block;
- }
-
- .status.info {
- background: #d1ecf1;
- color: #0c5460;
- border: 1px solid #bee5eb;
- }
-
- .status.success {
- background: #d4edda;
- color: #155724;
- border: 1px solid #c3e6cb;
- }
-
- .status.error {
- background: #f8d7da;
- color: #721c24;
- border: 1px solid #f5c6cb;
- }
-
- .output-section {
- margin-top: 30px;
- }
-
- .output-header {
- display: flex;
- justify-content: space-between;
- align-items: center;
- margin-bottom: 20px;
- padding-bottom: 15px;
- border-bottom: 2px solid #e1e8ed;
- }
-
- .output-title {
- font-size: 18px;
- font-weight: 600;
- color: #2c3e50;
- }
-
- .output-tabs {
- display: flex;
- gap: 5px;
- }
-
- .tab {
- padding: 8px 16px;
- cursor: pointer;
- border-radius: 4px;
- font-size: 14px;
- font-weight: 500;
- transition: all 0.3s;
- background: #f8f9fa;
- color: #6c757d;
- }
-
- .tab.active {
- background: #667eea;
- color: white;
- }
-
- .tab-content {
- display: none;
- }
-
- .tab-content.active {
- display: block;
- }
-
- .raw-output {
- background: #f8f9fa;
- border: 2px solid #e1e8ed;
- border-radius: 6px;
- padding: 20px;
- font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
- font-size: 13px;
- line-height: 1.5;
- white-space: pre-wrap;
- max-height: 500px;
- overflow-y: auto;
- color: #2c3e50;
- }
-
- .markdown-container {
- border: 2px solid #e1e8ed;
- border-radius: 6px;
- min-height: 400px;
- background: white;
- }
-
- /* 只读编辑器样式 */
- .markdown-container.readonly-editor {
- user-select: none; /* 禁用文本选择 */
- }
-
- .markdown-container .vditor-toolbar {
- display: none !important; /* 隐藏工具栏 */
- }
-
- .markdown-container .vditor-content {
- cursor: default !important; /* 默认光标 */
- }
-
- .markdown-container .vditor-content:focus {
- outline: none !important; /* 移除焦点样式 */
- }
-
- /* 确保滚动条正常工作 */
- .markdown-container .vditor-content {
- overflow-y: auto !important; /* 确保垂直滚动 */
- pointer-events: auto !important; /* 允许滚动交互 */
- }
-
- .markdown-container .vditor-content::-webkit-scrollbar {
- width: 8px;
- }
-
- .markdown-container .vditor-content::-webkit-scrollbar-track {
- background: #f1f1f1;
- border-radius: 4px;
- }
-
- .markdown-container .vditor-content::-webkit-scrollbar-thumb {
- background: #c1c1c1;
- border-radius: 4px;
- }
-
- .markdown-container .vditor-content::-webkit-scrollbar-thumb:hover {
- background: #a8a8a8;
- }
-
- .loading {
- display: none;
- text-align: center;
- padding: 40px;
- color: #6c757d;
- }
-
- .loading.show {
- display: block;
- }
-
- .spinner {
- border: 4px solid #f3f3f3;
- border-top: 4px solid #667eea;
- border-radius: 50%;
- width: 40px;
- height: 40px;
- animation: spin 1s linear infinite;
- margin: 0 auto 15px;
- }
-
- @keyframes spin {
- 0% { transform: rotate(0deg); }
- 100% { transform: rotate(360deg); }
- }
-
- .stats {
- display: flex;
- gap: 20px;
- margin-top: 15px;
- font-size: 12px;
- color: #6c757d;
- }
-
- .stat-item {
- display: flex;
- align-items: center;
- gap: 5px;
- }
-
- .stat-value {
- font-weight: 600;
- color: #2c3e50;
- }
-
- .db-info {
- background: #e8f5e8;
- border: 1px solid #c3e6cb;
- border-radius: 6px;
- padding: 15px;
- margin-bottom: 20px;
- }
-
- .db-info h4 {
- color: #155724;
- margin-bottom: 10px;
- }
-
- .db-info .info-item {
- margin-bottom: 5px;
- font-size: 14px;
- }
-
- .db-info .info-label {
- font-weight: 600;
- color: #2c3e50;
- }
- </style>
- </head>
- <body>
- <div class="container">
- <div class="header">
- <h1>🚀 流式聊天数据库集成测试</h1>
- <p>测试流式接口与数据库操作的集成功能</p>
- </div>
-
- <div class="content">
- <div class="test-section">
- <div class="form-group">
- <label for="apiUrl">API接口地址</label>
- <input type="text" id="apiUrl" value="http://localhost:22000/apiv1/stream/chat-with-db" placeholder="输入流式接口地址">
- </div>
-
- <div class="form-group">
- <label for="message">测试消息</label>
- <textarea id="message" placeholder="输入要测试的消息内容">你好,请介绍一下施工现场的安全防护要求,包括临边防护、脚手架管理等具体规范</textarea>
- </div>
-
- <div class="form-group">
- <label for="userId">用户ID</label>
- <input type="number" id="userId" value="1" placeholder="用户ID">
- </div>
-
- <div class="form-group">
- <label for="conversationId">对话ID (0表示新建对话)</label>
- <input type="number" id="conversationId" value="0" placeholder="对话ID">
- </div>
-
- <div class="form-group">
- <label for="businessType">业务类型</label>
- <input type="number" id="businessType" value="1" placeholder="业务类型">
- </div>
-
- <div class="form-group">
- <label for="examName">考试名称</label>
- <input type="text" id="examName" value="安全培训" placeholder="考试名称">
- </div>
-
- <div class="button-group">
- <button class="btn-primary" onclick="startTest()">▶️ 开始测试</button>
- <button class="btn-secondary" onclick="clearAll()">🗑️ 清空输出</button>
- <button class="btn-danger" onclick="stopTest()">⏹️ 停止测试</button>
- </div>
- </div>
-
- <div id="status" class="status"></div>
-
- <div class="loading" id="loading">
- <div class="spinner"></div>
- <div>正在接收流式数据...</div>
- </div>
-
- <!-- 数据库信息显示区域 -->
- <div id="dbInfo" class="db-info" style="display: none;">
- <h4>📊 数据库信息</h4>
- <div class="info-item">
- <span class="info-label">对话ID:</span>
- <span id="conversationIdDisplay">-</span>
- </div>
- <div class="info-item">
- <span class="info-label">消息ID:</span>
- <span id="messageIdDisplay">-</span>
- </div>
- <div class="info-item">
- <span class="info-label">状态:</span>
- <span id="dbStatusDisplay">-</span>
- </div>
- </div>
-
- <div class="output-section">
- <div class="output-header">
- <div class="output-title">📊 测试结果</div>
- <div class="output-tabs">
- <div class="tab active" onclick="switchTab('raw')">原始数据</div>
- <div class="tab" onclick="switchTab('wysiwyg')">WYSIWYG预览</div>
- <div class="tab" onclick="switchTab('ir')">即时渲染预览(IR)</div>
- <div class="tab" onclick="switchTab('sv')">分屏预览(SV)</div>
- </div>
- </div>
-
- <div style="background: #f8f9fa; padding: 15px; margin-bottom: 20px; border-radius: 6px; font-size: 14px; color: #6c757d;">
- <strong>📝 渲染模式说明(只读预览):</strong><br>
- • <strong>WYSIWYG预览</strong>:所见即所得预览,显示最终渲染效果(不可编辑)<br>
- • <strong>即时渲染预览(IR)</strong>:实时渲染Markdown为HTML预览(不可编辑)<br>
- • <strong>分屏预览(SV)</strong>:左侧显示Markdown源码,右侧显示HTML预览(不可编辑)
- </div>
-
- <div id="rawTab" class="tab-content active">
- <div class="raw-output" id="rawOutput">等待测试数据...</div>
- </div>
-
- <div id="wysiwygTab" class="tab-content">
- <div class="markdown-container readonly-editor" id="wysiwygOutput"></div>
- </div>
-
- <div id="irTab" class="tab-content">
- <div class="markdown-container readonly-editor" id="irOutput"></div>
- </div>
-
- <div id="svTab" class="tab-content">
- <div class="markdown-container readonly-editor" id="svOutput"></div>
- </div>
-
- <div class="stats" id="stats" style="display: none;">
- <div class="stat-item">
- <span>📝 字符数:</span>
- <span class="stat-value" id="charCount">0</span>
- </div>
- <div class="stat-item">
- <span>⏱️ 耗时:</span>
- <span class="stat-value" id="duration">0s</span>
- </div>
- <div class="stat-item">
- <span>📦 数据块:</span>
- <span class="stat-value" id="chunkCount">0</span>
- </div>
- </div>
- </div>
- </div>
- </div>
- <!-- Vditor JavaScript -->
- <script src="https://unpkg.com/vditor/dist/index.min.js"></script>
-
- <script>
- let vditorWysiwyg = null;
- let vditorIR = null;
- let vditorSV = null;
- let isStreaming = false;
- let startTime = null;
- let charCount = 0;
- let chunkCount = 0;
- let currentContent = '';
-
- // 防抖更新函数
- let debouncedUpdate = null;
-
- // 初始化防抖函数
- function initDebouncedUpdate() {
- debouncedUpdate = debounce((content) => {
- if (vditorWysiwyg) {
- vditorWysiwyg.setValue(content);
- }
- if (vditorIR) {
- vditorIR.setValue(content);
- }
- if (vditorSV) {
- vditorSV.setValue(content);
- }
- }, 30); // 30ms防抖,更流畅
- }
-
- // 防抖函数
- function debounce(func, wait) {
- let timeout;
- return function executedFunction(...args) {
- const later = () => {
- clearTimeout(timeout);
- func(...args);
- };
- clearTimeout(timeout);
- timeout = setTimeout(later, wait);
- };
- }
-
- // 初始化所有Vditor编辑器
- function initVditors() {
- // 销毁现有的编辑器
- if (vditorWysiwyg) vditorWysiwyg.destroy();
- if (vditorIR) vditorIR.destroy();
- if (vditorSV) vditorSV.destroy();
-
- // 初始化WYSIWYG模式 - 只读预览
- vditorWysiwyg = new Vditor('wysiwygOutput', {
- height: 400,
- mode: 'wysiwyg',
- cache: {
- id: 'vditor-wysiwyg-cache'
- },
- toolbar: [], // 去掉工具栏
- disabled: true, // 设置为只读
- after: () => {
- console.log('✅ WYSIWYG预览初始化完成');
- }
- });
-
- // 初始化即时渲染(IR)模式 - 只读预览
- vditorIR = new Vditor('irOutput', {
- height: 400,
- mode: 'ir',
- cache: {
- id: 'vditor-ir-cache'
- },
- toolbar: [], // 去掉工具栏
- disabled: true, // 设置为只读
- after: () => {
- console.log('✅ 即时渲染预览(IR)初始化完成');
- }
- });
-
- // 初始化分屏预览(SV)模式 - 只读预览
- vditorSV = new Vditor('svOutput', {
- height: 400,
- mode: 'sv',
- cache: {
- id: 'vditor-sv-cache'
- },
- toolbar: [], // 去掉工具栏
- disabled: true, // 设置为只读
- after: () => {
- console.log('✅ 分屏预览(SV)初始化完成');
- }
- });
- }
-
- // 切换标签页
- function switchTab(tabName) {
- // 隐藏所有标签内容
- document.querySelectorAll('.tab-content').forEach(content => {
- content.classList.remove('active');
- });
-
- // 移除所有标签的active类
- document.querySelectorAll('.tab').forEach(tab => {
- tab.classList.remove('active');
- });
-
- // 显示选中的标签内容
- document.getElementById(tabName + 'Tab').classList.add('active');
-
- // 添加active类到选中的标签
- event.target.classList.add('active');
- }
-
- // 显示状态信息
- function showStatus(message, type = 'info') {
- const statusEl = document.getElementById('status');
- statusEl.textContent = message;
- statusEl.className = `status ${type}`;
- statusEl.classList.add('show');
-
- setTimeout(() => {
- statusEl.classList.remove('show');
- }, 5000);
- }
-
- // 更新统计信息
- function updateStats() {
- const duration = startTime ? Math.round((Date.now() - startTime) / 1000) : 0;
- document.getElementById('charCount').textContent = charCount;
- document.getElementById('duration').textContent = duration + 's';
- document.getElementById('chunkCount').textContent = chunkCount;
- document.getElementById('stats').style.display = 'flex';
- }
-
- // 清空所有输出
- function clearAll() {
- document.getElementById('rawOutput').textContent = '等待测试数据...';
- currentContent = '';
-
- if (vditorWysiwyg) vditorWysiwyg.setValue('');
- if (vditorIR) vditorIR.setValue('');
- if (vditorSV) vditorSV.setValue('');
-
- charCount = 0;
- chunkCount = 0;
- startTime = null;
- updateStats();
-
- // 隐藏数据库信息
- document.getElementById('dbInfo').style.display = 'none';
-
- showStatus('✅ 输出已清空', 'success');
- }
-
- // 更新所有编辑器的内容
- function updateAllEditors(content) {
- currentContent = content;
-
- // 使用防抖更新
- if (debouncedUpdate) {
- debouncedUpdate(content);
- }
- }
-
- // 流式完成处理
- function onStreamComplete() {
- console.log('=' + '='.repeat(80));
- console.log('🎉 流式输出完成!');
- console.log('=' + '='.repeat(80));
- console.log('📊 统计信息:');
- console.log(`📝 总字符数: ${charCount}`);
- console.log(`📦 数据块数: ${chunkCount}`);
- console.log(`⏱️ 完成时间: ${new Date().toLocaleString()}`);
- console.log(`⏱️ 耗时: ${startTime ? Math.round((Date.now() - startTime) / 1000) : 0}秒`);
- console.log('=' + '='.repeat(80));
- console.log('📄 完整响应内容:');
- console.log('=' + '='.repeat(80));
- console.log(currentContent);
- console.log('=' + '='.repeat(80));
- console.log('🎯 原始数据内容:');
- console.log('=' + '='.repeat(80));
- console.log(document.getElementById('rawOutput').textContent);
- console.log('=' + '='.repeat(80));
- console.log('✅ 流式输出已完全结束,所有内容已渲染完成');
- }
-
- // 停止测试
- function stopTest() {
- isStreaming = false;
- document.getElementById('loading').classList.remove('show');
- showStatus('⏹️ 测试已停止', 'info');
- }
-
- // 开始测试
- function startTest() {
- const apiUrl = document.getElementById('apiUrl').value.trim();
- const message = document.getElementById('message').value.trim();
- const userId = parseInt(document.getElementById('userId').value) || 1;
- const conversationId = parseInt(document.getElementById('conversationId').value) || 0;
- const businessType = parseInt(document.getElementById('businessType').value) || 1;
- const examName = document.getElementById('examName').value.trim();
-
- if (!apiUrl || !message) {
- showStatus('❌ 请填写API地址和测试消息', 'error');
- return;
- }
-
- // 停止之前的测试
- stopTest();
-
- // 清空输出
- clearAll();
-
- // 显示加载状态
- document.getElementById('loading').classList.add('show');
- isStreaming = true;
- startTime = Date.now();
-
- showStatus('🔄 正在连接流式接口...', 'info');
-
- // 构建请求数据
- const requestData = {
- message: message,
- user_id: userId,
- ai_conversation_id: conversationId,
- business_type: businessType,
- exam_name: examName
- };
-
- console.log('📤 发送请求:', requestData);
-
- // 使用fetch发送POST请求
- fetch(apiUrl, {
- method: 'POST',
- headers: {
- 'Content-Type': 'application/json',
- },
- body: JSON.stringify(requestData)
- })
- .then(response => {
- if (!response.ok) {
- throw new Error(`HTTP错误: ${response.status} ${response.statusText}`);
- }
-
- if (!response.body) {
- throw new Error('响应体为空');
- }
-
- showStatus('✅ 连接成功,开始接收流式数据...', 'success');
-
- // 处理流式响应
- const reader = response.body.getReader();
- const decoder = new TextDecoder();
- let buffer = '';
- let rawContent = '';
- let markdownContent = '';
-
- function readStream() {
- reader.read().then(({ done, value }) => {
- if (done) {
- console.log('✅ 流式数据接收完成');
- document.getElementById('loading').classList.remove('show');
- isStreaming = false;
- showStatus('✅ 流式数据接收完成', 'success');
- updateStats();
- onStreamComplete();
- return;
- }
-
- // 解码数据
- const chunk = decoder.decode(value, { stream: true });
- buffer += chunk;
-
- // 处理完整的数据行
- const lines = buffer.split('\n');
- buffer = lines.pop() || ''; // 保留最后一个不完整的行
-
- for (const line of lines) {
- if (line.trim() === '') continue;
-
- console.log('📥 收到数据行:', line);
-
- if (line.startsWith('data: ')) {
- const data = line.substring(6);
-
- if (data === '[DONE]') {
- console.log('🏁 流式结束');
- document.getElementById('loading').classList.remove('show');
- isStreaming = false;
- showStatus('✅ 流式数据接收完成', 'success');
- updateStats();
- onStreamComplete();
- return;
- }
-
- // 尝试解析JSON
- try {
- const jsonData = JSON.parse(data);
-
- // 处理初始响应(数据库ID)
- if (jsonData.type === 'initial') {
- console.log('📊 收到数据库信息:', jsonData);
- document.getElementById('conversationIdDisplay').textContent = jsonData.ai_conversation_id;
- document.getElementById('messageIdDisplay').textContent = jsonData.ai_message_id;
- document.getElementById('dbStatusDisplay').textContent = jsonData.status;
- document.getElementById('dbInfo').style.display = 'block';
- showStatus('✅ 数据库操作成功,开始流式输出', 'success');
- continue;
- }
-
- if (jsonData.error) {
- showStatus(`❌ 错误: ${jsonData.error}`, 'error');
- document.getElementById('loading').classList.remove('show');
- isStreaming = false;
- return;
- }
- } catch (e) {
- // 不是JSON,直接作为文本内容处理
- console.log('📝 收到文本内容:', data);
-
- chunkCount++;
-
- // 处理转义的换行符,将\n转换回真正的换行符
- const processedData = data.replace(/\\n/g, '\n');
-
- // 添加到原始输出
- rawContent += processedData;
- document.getElementById('rawOutput').textContent = rawContent;
-
- // 添加到markdown内容
- markdownContent += processedData;
- charCount += processedData.length;
-
- // 实时更新所有编辑器
- updateAllEditors(markdownContent);
-
- // 更新统计信息
- updateStats();
- }
- }
- }
-
- // 继续读取
- readStream();
- }).catch(error => {
- console.error('❌ 读取流式数据时出错:', error);
- showStatus(`❌ 读取数据出错: ${error.message}`, 'error');
- document.getElementById('loading').classList.remove('show');
- isStreaming = false;
- });
- }
-
- readStream();
-
- })
- .catch(error => {
- console.error('❌ 请求失败:', error);
- showStatus(`❌ 请求失败: ${error.message}`, 'error');
- document.getElementById('loading').classList.remove('show');
- isStreaming = false;
- });
- }
-
- // 页面加载完成后初始化
- document.addEventListener('DOMContentLoaded', function() {
- initVditors();
- initDebouncedUpdate(); // 初始化防抖函数
- showStatus('✅ 页面加载完成,可以开始测试', 'success');
- });
-
- // 页面卸载时清理
- window.addEventListener('beforeunload', function() {
- stopTest();
- });
- </script>
- </body>
- </html>
|