stream_chat_with_db_test.html 30 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844
  1. <!DOCTYPE html>
  2. <html lang="zh-CN">
  3. <head>
  4. <meta charset="UTF-8">
  5. <meta name="viewport" content="width=device-width, initial-scale=1.0">
  6. <title>流式聊天数据库集成测试</title>
  7. <!-- Vditor CSS -->
  8. <link rel="stylesheet" href="https://unpkg.com/vditor/dist/index.css" />
  9. <style>
  10. * {
  11. margin: 0;
  12. padding: 0;
  13. box-sizing: border-box;
  14. }
  15. body {
  16. font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
  17. background: #f5f5f5;
  18. padding: 20px;
  19. }
  20. .container {
  21. max-width: 1000px;
  22. margin: 0 auto;
  23. background: white;
  24. border-radius: 8px;
  25. box-shadow: 0 2px 10px rgba(0,0,0,0.1);
  26. overflow: hidden;
  27. }
  28. .header {
  29. background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
  30. color: white;
  31. padding: 30px;
  32. text-align: center;
  33. }
  34. .header h1 {
  35. font-size: 28px;
  36. margin-bottom: 10px;
  37. }
  38. .header p {
  39. opacity: 0.9;
  40. font-size: 16px;
  41. }
  42. .content {
  43. padding: 30px;
  44. }
  45. .test-section {
  46. margin-bottom: 30px;
  47. padding: 25px;
  48. background: #f8f9fa;
  49. border-radius: 8px;
  50. border-left: 4px solid #667eea;
  51. }
  52. .form-group {
  53. margin-bottom: 20px;
  54. }
  55. label {
  56. display: block;
  57. margin-bottom: 8px;
  58. font-weight: 600;
  59. color: #2c3e50;
  60. font-size: 14px;
  61. }
  62. input, textarea {
  63. width: 100%;
  64. padding: 12px;
  65. border: 2px solid #e1e8ed;
  66. border-radius: 6px;
  67. font-size: 14px;
  68. transition: border-color 0.3s;
  69. }
  70. input:focus, textarea:focus {
  71. outline: none;
  72. border-color: #667eea;
  73. }
  74. textarea {
  75. height: 100px;
  76. resize: vertical;
  77. font-family: inherit;
  78. }
  79. .button-group {
  80. display: flex;
  81. gap: 15px;
  82. margin-top: 20px;
  83. }
  84. button {
  85. padding: 12px 24px;
  86. border: none;
  87. border-radius: 6px;
  88. cursor: pointer;
  89. font-size: 14px;
  90. font-weight: 600;
  91. transition: all 0.3s;
  92. min-width: 120px;
  93. }
  94. .btn-primary {
  95. background: #667eea;
  96. color: white;
  97. }
  98. .btn-primary:hover {
  99. background: #5a6fd8;
  100. transform: translateY(-1px);
  101. }
  102. .btn-secondary {
  103. background: #6c757d;
  104. color: white;
  105. }
  106. .btn-secondary:hover {
  107. background: #5a6268;
  108. }
  109. .btn-danger {
  110. background: #dc3545;
  111. color: white;
  112. }
  113. .btn-danger:hover {
  114. background: #c82333;
  115. }
  116. .status {
  117. padding: 15px;
  118. margin: 15px 0;
  119. border-radius: 6px;
  120. font-weight: 600;
  121. display: none;
  122. }
  123. .status.show {
  124. display: block;
  125. }
  126. .status.info {
  127. background: #d1ecf1;
  128. color: #0c5460;
  129. border: 1px solid #bee5eb;
  130. }
  131. .status.success {
  132. background: #d4edda;
  133. color: #155724;
  134. border: 1px solid #c3e6cb;
  135. }
  136. .status.error {
  137. background: #f8d7da;
  138. color: #721c24;
  139. border: 1px solid #f5c6cb;
  140. }
  141. .output-section {
  142. margin-top: 30px;
  143. }
  144. .output-header {
  145. display: flex;
  146. justify-content: space-between;
  147. align-items: center;
  148. margin-bottom: 20px;
  149. padding-bottom: 15px;
  150. border-bottom: 2px solid #e1e8ed;
  151. }
  152. .output-title {
  153. font-size: 18px;
  154. font-weight: 600;
  155. color: #2c3e50;
  156. }
  157. .output-tabs {
  158. display: flex;
  159. gap: 5px;
  160. }
  161. .tab {
  162. padding: 8px 16px;
  163. cursor: pointer;
  164. border-radius: 4px;
  165. font-size: 14px;
  166. font-weight: 500;
  167. transition: all 0.3s;
  168. background: #f8f9fa;
  169. color: #6c757d;
  170. }
  171. .tab.active {
  172. background: #667eea;
  173. color: white;
  174. }
  175. .tab-content {
  176. display: none;
  177. }
  178. .tab-content.active {
  179. display: block;
  180. }
  181. .raw-output {
  182. background: #f8f9fa;
  183. border: 2px solid #e1e8ed;
  184. border-radius: 6px;
  185. padding: 20px;
  186. font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
  187. font-size: 13px;
  188. line-height: 1.5;
  189. white-space: pre-wrap;
  190. max-height: 500px;
  191. overflow-y: auto;
  192. color: #2c3e50;
  193. }
  194. .markdown-container {
  195. border: 2px solid #e1e8ed;
  196. border-radius: 6px;
  197. min-height: 400px;
  198. background: white;
  199. }
  200. /* 只读编辑器样式 */
  201. .markdown-container.readonly-editor {
  202. user-select: none; /* 禁用文本选择 */
  203. }
  204. .markdown-container .vditor-toolbar {
  205. display: none !important; /* 隐藏工具栏 */
  206. }
  207. .markdown-container .vditor-content {
  208. cursor: default !important; /* 默认光标 */
  209. }
  210. .markdown-container .vditor-content:focus {
  211. outline: none !important; /* 移除焦点样式 */
  212. }
  213. /* 确保滚动条正常工作 */
  214. .markdown-container .vditor-content {
  215. overflow-y: auto !important; /* 确保垂直滚动 */
  216. pointer-events: auto !important; /* 允许滚动交互 */
  217. }
  218. .markdown-container .vditor-content::-webkit-scrollbar {
  219. width: 8px;
  220. }
  221. .markdown-container .vditor-content::-webkit-scrollbar-track {
  222. background: #f1f1f1;
  223. border-radius: 4px;
  224. }
  225. .markdown-container .vditor-content::-webkit-scrollbar-thumb {
  226. background: #c1c1c1;
  227. border-radius: 4px;
  228. }
  229. .markdown-container .vditor-content::-webkit-scrollbar-thumb:hover {
  230. background: #a8a8a8;
  231. }
  232. .loading {
  233. display: none;
  234. text-align: center;
  235. padding: 40px;
  236. color: #6c757d;
  237. }
  238. .loading.show {
  239. display: block;
  240. }
  241. .spinner {
  242. border: 4px solid #f3f3f3;
  243. border-top: 4px solid #667eea;
  244. border-radius: 50%;
  245. width: 40px;
  246. height: 40px;
  247. animation: spin 1s linear infinite;
  248. margin: 0 auto 15px;
  249. }
  250. @keyframes spin {
  251. 0% { transform: rotate(0deg); }
  252. 100% { transform: rotate(360deg); }
  253. }
  254. .stats {
  255. display: flex;
  256. gap: 20px;
  257. margin-top: 15px;
  258. font-size: 12px;
  259. color: #6c757d;
  260. }
  261. .stat-item {
  262. display: flex;
  263. align-items: center;
  264. gap: 5px;
  265. }
  266. .stat-value {
  267. font-weight: 600;
  268. color: #2c3e50;
  269. }
  270. .db-info {
  271. background: #e8f5e8;
  272. border: 1px solid #c3e6cb;
  273. border-radius: 6px;
  274. padding: 15px;
  275. margin-bottom: 20px;
  276. }
  277. .db-info h4 {
  278. color: #155724;
  279. margin-bottom: 10px;
  280. }
  281. .db-info .info-item {
  282. margin-bottom: 5px;
  283. font-size: 14px;
  284. }
  285. .db-info .info-label {
  286. font-weight: 600;
  287. color: #2c3e50;
  288. }
  289. </style>
  290. </head>
  291. <body>
  292. <div class="container">
  293. <div class="header">
  294. <h1>🚀 流式聊天数据库集成测试</h1>
  295. <p>测试流式接口与数据库操作的集成功能</p>
  296. </div>
  297. <div class="content">
  298. <div class="test-section">
  299. <div class="form-group">
  300. <label for="apiUrl">API接口地址</label>
  301. <input type="text" id="apiUrl" value="http://localhost:22000/apiv1/stream/chat-with-db" placeholder="输入流式接口地址">
  302. </div>
  303. <div class="form-group">
  304. <label for="message">测试消息</label>
  305. <textarea id="message" placeholder="输入要测试的消息内容">你好,请介绍一下施工现场的安全防护要求,包括临边防护、脚手架管理等具体规范</textarea>
  306. </div>
  307. <div class="form-group">
  308. <label for="userId">用户ID</label>
  309. <input type="number" id="userId" value="1" placeholder="用户ID">
  310. </div>
  311. <div class="form-group">
  312. <label for="conversationId">对话ID (0表示新建对话)</label>
  313. <input type="number" id="conversationId" value="0" placeholder="对话ID">
  314. </div>
  315. <div class="form-group">
  316. <label for="businessType">业务类型</label>
  317. <input type="number" id="businessType" value="1" placeholder="业务类型">
  318. </div>
  319. <div class="form-group">
  320. <label for="examName">考试名称</label>
  321. <input type="text" id="examName" value="安全培训" placeholder="考试名称">
  322. </div>
  323. <div class="button-group">
  324. <button class="btn-primary" onclick="startTest()">▶️ 开始测试</button>
  325. <button class="btn-secondary" onclick="clearAll()">🗑️ 清空输出</button>
  326. <button class="btn-danger" onclick="stopTest()">⏹️ 停止测试</button>
  327. </div>
  328. </div>
  329. <div id="status" class="status"></div>
  330. <div class="loading" id="loading">
  331. <div class="spinner"></div>
  332. <div>正在接收流式数据...</div>
  333. </div>
  334. <!-- 数据库信息显示区域 -->
  335. <div id="dbInfo" class="db-info" style="display: none;">
  336. <h4>📊 数据库信息</h4>
  337. <div class="info-item">
  338. <span class="info-label">对话ID:</span>
  339. <span id="conversationIdDisplay">-</span>
  340. </div>
  341. <div class="info-item">
  342. <span class="info-label">消息ID:</span>
  343. <span id="messageIdDisplay">-</span>
  344. </div>
  345. <div class="info-item">
  346. <span class="info-label">状态:</span>
  347. <span id="dbStatusDisplay">-</span>
  348. </div>
  349. </div>
  350. <div class="output-section">
  351. <div class="output-header">
  352. <div class="output-title">📊 测试结果</div>
  353. <div class="output-tabs">
  354. <div class="tab active" onclick="switchTab('raw')">原始数据</div>
  355. <div class="tab" onclick="switchTab('wysiwyg')">WYSIWYG预览</div>
  356. <div class="tab" onclick="switchTab('ir')">即时渲染预览(IR)</div>
  357. <div class="tab" onclick="switchTab('sv')">分屏预览(SV)</div>
  358. </div>
  359. </div>
  360. <div style="background: #f8f9fa; padding: 15px; margin-bottom: 20px; border-radius: 6px; font-size: 14px; color: #6c757d;">
  361. <strong>📝 渲染模式说明(只读预览):</strong><br>
  362. • <strong>WYSIWYG预览</strong>:所见即所得预览,显示最终渲染效果(不可编辑)<br>
  363. • <strong>即时渲染预览(IR)</strong>:实时渲染Markdown为HTML预览(不可编辑)<br>
  364. • <strong>分屏预览(SV)</strong>:左侧显示Markdown源码,右侧显示HTML预览(不可编辑)
  365. </div>
  366. <div id="rawTab" class="tab-content active">
  367. <div class="raw-output" id="rawOutput">等待测试数据...</div>
  368. </div>
  369. <div id="wysiwygTab" class="tab-content">
  370. <div class="markdown-container readonly-editor" id="wysiwygOutput"></div>
  371. </div>
  372. <div id="irTab" class="tab-content">
  373. <div class="markdown-container readonly-editor" id="irOutput"></div>
  374. </div>
  375. <div id="svTab" class="tab-content">
  376. <div class="markdown-container readonly-editor" id="svOutput"></div>
  377. </div>
  378. <div class="stats" id="stats" style="display: none;">
  379. <div class="stat-item">
  380. <span>📝 字符数:</span>
  381. <span class="stat-value" id="charCount">0</span>
  382. </div>
  383. <div class="stat-item">
  384. <span>⏱️ 耗时:</span>
  385. <span class="stat-value" id="duration">0s</span>
  386. </div>
  387. <div class="stat-item">
  388. <span>📦 数据块:</span>
  389. <span class="stat-value" id="chunkCount">0</span>
  390. </div>
  391. </div>
  392. </div>
  393. </div>
  394. </div>
  395. <!-- Vditor JavaScript -->
  396. <script src="https://unpkg.com/vditor/dist/index.min.js"></script>
  397. <script>
  398. let vditorWysiwyg = null;
  399. let vditorIR = null;
  400. let vditorSV = null;
  401. let isStreaming = false;
  402. let startTime = null;
  403. let charCount = 0;
  404. let chunkCount = 0;
  405. let currentContent = '';
  406. // 防抖更新函数
  407. let debouncedUpdate = null;
  408. // 初始化防抖函数
  409. function initDebouncedUpdate() {
  410. debouncedUpdate = debounce((content) => {
  411. if (vditorWysiwyg) {
  412. vditorWysiwyg.setValue(content);
  413. }
  414. if (vditorIR) {
  415. vditorIR.setValue(content);
  416. }
  417. if (vditorSV) {
  418. vditorSV.setValue(content);
  419. }
  420. }, 30); // 30ms防抖,更流畅
  421. }
  422. // 防抖函数
  423. function debounce(func, wait) {
  424. let timeout;
  425. return function executedFunction(...args) {
  426. const later = () => {
  427. clearTimeout(timeout);
  428. func(...args);
  429. };
  430. clearTimeout(timeout);
  431. timeout = setTimeout(later, wait);
  432. };
  433. }
  434. // 初始化所有Vditor编辑器
  435. function initVditors() {
  436. // 销毁现有的编辑器
  437. if (vditorWysiwyg) vditorWysiwyg.destroy();
  438. if (vditorIR) vditorIR.destroy();
  439. if (vditorSV) vditorSV.destroy();
  440. // 初始化WYSIWYG模式 - 只读预览
  441. vditorWysiwyg = new Vditor('wysiwygOutput', {
  442. height: 400,
  443. mode: 'wysiwyg',
  444. cache: {
  445. id: 'vditor-wysiwyg-cache'
  446. },
  447. toolbar: [], // 去掉工具栏
  448. disabled: true, // 设置为只读
  449. after: () => {
  450. console.log('✅ WYSIWYG预览初始化完成');
  451. }
  452. });
  453. // 初始化即时渲染(IR)模式 - 只读预览
  454. vditorIR = new Vditor('irOutput', {
  455. height: 400,
  456. mode: 'ir',
  457. cache: {
  458. id: 'vditor-ir-cache'
  459. },
  460. toolbar: [], // 去掉工具栏
  461. disabled: true, // 设置为只读
  462. after: () => {
  463. console.log('✅ 即时渲染预览(IR)初始化完成');
  464. }
  465. });
  466. // 初始化分屏预览(SV)模式 - 只读预览
  467. vditorSV = new Vditor('svOutput', {
  468. height: 400,
  469. mode: 'sv',
  470. cache: {
  471. id: 'vditor-sv-cache'
  472. },
  473. toolbar: [], // 去掉工具栏
  474. disabled: true, // 设置为只读
  475. after: () => {
  476. console.log('✅ 分屏预览(SV)初始化完成');
  477. }
  478. });
  479. }
  480. // 切换标签页
  481. function switchTab(tabName) {
  482. // 隐藏所有标签内容
  483. document.querySelectorAll('.tab-content').forEach(content => {
  484. content.classList.remove('active');
  485. });
  486. // 移除所有标签的active类
  487. document.querySelectorAll('.tab').forEach(tab => {
  488. tab.classList.remove('active');
  489. });
  490. // 显示选中的标签内容
  491. document.getElementById(tabName + 'Tab').classList.add('active');
  492. // 添加active类到选中的标签
  493. event.target.classList.add('active');
  494. }
  495. // 显示状态信息
  496. function showStatus(message, type = 'info') {
  497. const statusEl = document.getElementById('status');
  498. statusEl.textContent = message;
  499. statusEl.className = `status ${type}`;
  500. statusEl.classList.add('show');
  501. setTimeout(() => {
  502. statusEl.classList.remove('show');
  503. }, 5000);
  504. }
  505. // 更新统计信息
  506. function updateStats() {
  507. const duration = startTime ? Math.round((Date.now() - startTime) / 1000) : 0;
  508. document.getElementById('charCount').textContent = charCount;
  509. document.getElementById('duration').textContent = duration + 's';
  510. document.getElementById('chunkCount').textContent = chunkCount;
  511. document.getElementById('stats').style.display = 'flex';
  512. }
  513. // 清空所有输出
  514. function clearAll() {
  515. document.getElementById('rawOutput').textContent = '等待测试数据...';
  516. currentContent = '';
  517. if (vditorWysiwyg) vditorWysiwyg.setValue('');
  518. if (vditorIR) vditorIR.setValue('');
  519. if (vditorSV) vditorSV.setValue('');
  520. charCount = 0;
  521. chunkCount = 0;
  522. startTime = null;
  523. updateStats();
  524. // 隐藏数据库信息
  525. document.getElementById('dbInfo').style.display = 'none';
  526. showStatus('✅ 输出已清空', 'success');
  527. }
  528. // 更新所有编辑器的内容
  529. function updateAllEditors(content) {
  530. currentContent = content;
  531. // 使用防抖更新
  532. if (debouncedUpdate) {
  533. debouncedUpdate(content);
  534. }
  535. }
  536. // 流式完成处理
  537. function onStreamComplete() {
  538. console.log('=' + '='.repeat(80));
  539. console.log('🎉 流式输出完成!');
  540. console.log('=' + '='.repeat(80));
  541. console.log('📊 统计信息:');
  542. console.log(`📝 总字符数: ${charCount}`);
  543. console.log(`📦 数据块数: ${chunkCount}`);
  544. console.log(`⏱️ 完成时间: ${new Date().toLocaleString()}`);
  545. console.log(`⏱️ 耗时: ${startTime ? Math.round((Date.now() - startTime) / 1000) : 0}秒`);
  546. console.log('=' + '='.repeat(80));
  547. console.log('📄 完整响应内容:');
  548. console.log('=' + '='.repeat(80));
  549. console.log(currentContent);
  550. console.log('=' + '='.repeat(80));
  551. console.log('🎯 原始数据内容:');
  552. console.log('=' + '='.repeat(80));
  553. console.log(document.getElementById('rawOutput').textContent);
  554. console.log('=' + '='.repeat(80));
  555. console.log('✅ 流式输出已完全结束,所有内容已渲染完成');
  556. }
  557. // 停止测试
  558. function stopTest() {
  559. isStreaming = false;
  560. document.getElementById('loading').classList.remove('show');
  561. showStatus('⏹️ 测试已停止', 'info');
  562. }
  563. // 开始测试
  564. function startTest() {
  565. const apiUrl = document.getElementById('apiUrl').value.trim();
  566. const message = document.getElementById('message').value.trim();
  567. const userId = parseInt(document.getElementById('userId').value) || 1;
  568. const conversationId = parseInt(document.getElementById('conversationId').value) || 0;
  569. const businessType = parseInt(document.getElementById('businessType').value) || 1;
  570. const examName = document.getElementById('examName').value.trim();
  571. if (!apiUrl || !message) {
  572. showStatus('❌ 请填写API地址和测试消息', 'error');
  573. return;
  574. }
  575. // 停止之前的测试
  576. stopTest();
  577. // 清空输出
  578. clearAll();
  579. // 显示加载状态
  580. document.getElementById('loading').classList.add('show');
  581. isStreaming = true;
  582. startTime = Date.now();
  583. showStatus('🔄 正在连接流式接口...', 'info');
  584. // 构建请求数据
  585. const requestData = {
  586. message: message,
  587. user_id: userId,
  588. ai_conversation_id: conversationId,
  589. business_type: businessType,
  590. exam_name: examName
  591. };
  592. console.log('📤 发送请求:', requestData);
  593. // 使用fetch发送POST请求
  594. fetch(apiUrl, {
  595. method: 'POST',
  596. headers: {
  597. 'Content-Type': 'application/json',
  598. },
  599. body: JSON.stringify(requestData)
  600. })
  601. .then(response => {
  602. if (!response.ok) {
  603. throw new Error(`HTTP错误: ${response.status} ${response.statusText}`);
  604. }
  605. if (!response.body) {
  606. throw new Error('响应体为空');
  607. }
  608. showStatus('✅ 连接成功,开始接收流式数据...', 'success');
  609. // 处理流式响应
  610. const reader = response.body.getReader();
  611. const decoder = new TextDecoder();
  612. let buffer = '';
  613. let rawContent = '';
  614. let markdownContent = '';
  615. function readStream() {
  616. reader.read().then(({ done, value }) => {
  617. if (done) {
  618. console.log('✅ 流式数据接收完成');
  619. document.getElementById('loading').classList.remove('show');
  620. isStreaming = false;
  621. showStatus('✅ 流式数据接收完成', 'success');
  622. updateStats();
  623. onStreamComplete();
  624. return;
  625. }
  626. // 解码数据
  627. const chunk = decoder.decode(value, { stream: true });
  628. buffer += chunk;
  629. // 处理完整的数据行
  630. const lines = buffer.split('\n');
  631. buffer = lines.pop() || ''; // 保留最后一个不完整的行
  632. for (const line of lines) {
  633. if (line.trim() === '') continue;
  634. console.log('📥 收到数据行:', line);
  635. if (line.startsWith('data: ')) {
  636. const data = line.substring(6);
  637. if (data === '[DONE]') {
  638. console.log('🏁 流式结束');
  639. document.getElementById('loading').classList.remove('show');
  640. isStreaming = false;
  641. showStatus('✅ 流式数据接收完成', 'success');
  642. updateStats();
  643. onStreamComplete();
  644. return;
  645. }
  646. // 尝试解析JSON
  647. try {
  648. const jsonData = JSON.parse(data);
  649. // 处理初始响应(数据库ID)
  650. if (jsonData.type === 'initial') {
  651. console.log('📊 收到数据库信息:', jsonData);
  652. document.getElementById('conversationIdDisplay').textContent = jsonData.ai_conversation_id;
  653. document.getElementById('messageIdDisplay').textContent = jsonData.ai_message_id;
  654. document.getElementById('dbStatusDisplay').textContent = jsonData.status;
  655. document.getElementById('dbInfo').style.display = 'block';
  656. showStatus('✅ 数据库操作成功,开始流式输出', 'success');
  657. continue;
  658. }
  659. if (jsonData.error) {
  660. showStatus(`❌ 错误: ${jsonData.error}`, 'error');
  661. document.getElementById('loading').classList.remove('show');
  662. isStreaming = false;
  663. return;
  664. }
  665. } catch (e) {
  666. // 不是JSON,直接作为文本内容处理
  667. console.log('📝 收到文本内容:', data);
  668. chunkCount++;
  669. // 处理转义的换行符,将\n转换回真正的换行符
  670. const processedData = data.replace(/\\n/g, '\n');
  671. // 添加到原始输出
  672. rawContent += processedData;
  673. document.getElementById('rawOutput').textContent = rawContent;
  674. // 添加到markdown内容
  675. markdownContent += processedData;
  676. charCount += processedData.length;
  677. // 实时更新所有编辑器
  678. updateAllEditors(markdownContent);
  679. // 更新统计信息
  680. updateStats();
  681. }
  682. }
  683. }
  684. // 继续读取
  685. readStream();
  686. }).catch(error => {
  687. console.error('❌ 读取流式数据时出错:', error);
  688. showStatus(`❌ 读取数据出错: ${error.message}`, 'error');
  689. document.getElementById('loading').classList.remove('show');
  690. isStreaming = false;
  691. });
  692. }
  693. readStream();
  694. })
  695. .catch(error => {
  696. console.error('❌ 请求失败:', error);
  697. showStatus(`❌ 请求失败: ${error.message}`, 'error');
  698. document.getElementById('loading').classList.remove('show');
  699. isStreaming = false;
  700. });
  701. }
  702. // 页面加载完成后初始化
  703. document.addEventListener('DOMContentLoaded', function() {
  704. initVditors();
  705. initDebouncedUpdate(); // 初始化防抖函数
  706. showStatus('✅ 页面加载完成,可以开始测试', 'success');
  707. });
  708. // 页面卸载时清理
  709. window.addEventListener('beforeunload', function() {
  710. stopTest();
  711. });
  712. </script>
  713. </body>
  714. </html>