1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899
| from flask import Flask, render_template, request, jsonify import os import requests import time import threading
app = Flask(__name__)
# 预定义的响应文本 responses = { "btn1": "你好!欢迎使用这个文本逐字显示演示。这是按钮1的响应内容。", "btn2": "这是按钮2的响应内容。逐字显示效果可以让信息呈现更加生动有趣。", "btn3": "按钮3被点击了!这个功能可以用来创建聊天机器人或者交互式故事。", "btn4": "感谢您测试这个功能。逐字显示模拟了人类打字的效果,增强了用户体验。" }
# DeepSeek API 配置 #DEEPSEEK_API_URL = "https://api.deepseek.com/" # 请确认实际API地址 DEEPSEEK_API_URL = "https://api.deepseek.com/chat/completions" # 请确认实际API地址 DEEPSEEK_API_KEY = os.getenv('DEEPSEEK_API_KEY') # 从系统环境变量读取
@app.route('/') def index(): return render_template('index.html')
@app.route('/get_response', methods=['POST']) def get_response(): data = request.json button_id = data.get('button_id') response_text = responses.get(button_id, "未知按钮") # 返回整个文本,前端会处理逐字显示 return jsonify({"text": response_text})
@app.route('/call_deepseek', methods=['POST']) def call_deepseek(): user_input = request.json.get('user_input') if not user_input: return jsonify({"error": "No input provided"}), 400 try: # 调用 DeepSeek API headers = { "Authorization": f"Bearer {DEEPSEEK_API_KEY}", "Content-Type": "application/json" } payload = { "model": "deepseek-v4-pro", # 根据实际情况调整 "messages": [ {"role": "user", "content": user_input} ], # "temperature": 0.7 "thinking": {"type": "enabled"}, "reasoning_effort": "high", "stream": False } response = requests.post(DEEPSEEK_API_URL, json=payload, headers=headers) response.raise_for_status() # 解析响应 - 根据DeepSeek API实际返回结构调整 api_response = response.json() reply_content = api_response['choices'][0]['message']['content'] return jsonify({"text": reply_content}) except requests.exceptions.RequestException as e: return jsonify({"error": f"API调用失败: {str(e)}"}), 500
if __name__ == '__main__': app.run(debug=True)
改用流式,其他地方如何相应调整 我们被问到一个问题:代码中有一个Flask应用,调用DeepSeek API目前的请求是非流式的("stream": False)。用户希望改用流式,问“其他地方如何相应调整”。需要分析前端和后端的修改。前端负责接收流式数据并逐步显示,后端需要返回流式响应(SSE或分块传输)。用户给出了后端代码,但没有前端代码。我们需要提供调整建议。输出应该是中文,详细说明后端和前端需要改动的地方。
要将 DeepSeek API 调用改为流式(stream: True),你的后端和前端都需要做相应调整。下面我会分别说明具体的改动方法。
一、后端调整(Flask) 当前代码是非流式的一次性请求,返回完整 JSON。改为流式后,需要:
修改 payload:将 "stream": False 改为 "stream": True(可以省略该字段,默认为 False,所以必须显式设为 True)。
修改响应方式:后端不能直接 return jsonify(...),而要返回一个流式响应(Response 对象,设置 content_type='text/event-stream' 或 'text/plain'),并逐步向前端发送数据块(data chunks)。
逐块处理 API 响应流:DeepSeek API 会返回 SSE(Server-Sent Events)格式的数据,每行以 data: 开头,最后以 data: [DONE] 结束。你需要解析这些行,提取出每次的内容增量(delta),再转发给前端。
修改后的后端代码示例 python from flask import Flask, render_template, request, Response, jsonify import os import requests import json
@app.route('/call_deepseek_stream', methods=['POST']) def call_deepseek_stream(): user_input = request.json.get('user_input') if not user_input: return jsonify({"error": "No input provided"}), 400
def generate(): headers = { "Authorization": f"Bearer {DEEPSEEK_API_KEY}", "Content-Type": "application/json" } payload = { "model": "deepseek-v4-pro", "messages": [{"role": "user", "content": user_input}], "stream": True, # 启用流式 "thinking": {"type": "enabled"}, "reasoning_effort": "high" } try: # 发送流式请求 response = requests.post(DEEPSEEK_API_URL, json=payload, headers=headers, stream=True) response.raise_for_status()
# 逐行读取 API 返回的 SSE 数据 for line in response.iter_lines(decode_unicode=True): if line: # 去掉 "data: " 前缀 if line.startswith('data: '): data_str = line[6:] else: data_str = line
# 检查结束标志 if data_str == '[DONE]': break
try: chunk = json.loads(data_str) # 根据 DeepSeek API 响应结构提取内容增量 delta = chunk['choices'][0]['delta'] content = delta.get('content', '') if content: # 将每段内容发送给前端(SSE 格式) yield f"data: {json.dumps({'text': content})}\n\n" except json.JSONDecodeError: continue except Exception as e: yield f"data: {json.dumps({'error': str(e)})}\n\n"
return Response(generate(), content_type='text/event-stream') 说明:
使用 requests.post(..., stream=True) 保持连接,逐行读取响应。
用生成器 generate() 不断 yield 符合 SSE 格式的数据给前端。
每收到一个 delta 中的 content,就立即转发。
前端通过 EventSource 或 fetch + ReadableStream 接收。
二、前端调整 前端原本可能是用 fetch 或 axios 等待完整 JSON 响应。流式场景下需要改为逐块接收并实时更新页面文字。
1. 使用 fetch + ReadableStream(推荐) javascript async function callDeepSeekStream(userInput) { const response = await fetch('/call_deepseek_stream', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ user_input: userInput }) });
const reader = response.body.getReader(); const decoder = new TextDecoder('utf-8'); let buffer = '';
while (true) { const { done, value } = await reader.read(); if (done) break;
buffer += decoder.decode(value, { stream: true }); // 按行分割 SSE 消息(每条消息以 \n\n 结束) const lines = buffer.split('\n\n'); buffer = lines.pop(); // 剩余未完整的数据留到下次
for (const line of lines) { if (line.startsWith('data: ')) { const dataStr = line.slice(6); if (dataStr === '[DONE]') continue; try { const data = JSON.parse(dataStr); if (data.text) { // 累加显示到页面上(例如追加到某个 div) appendTypingText(data.text); // 这里需要你自己实现累加逐字效果 } if (data.error) { console.error(data.error); } } catch (e) { console.warn('Parse error', e); } } } } } 2. 与现有“逐字显示”逻辑的配合 你之前的前端应该有一个函数(例如 displayTextCharacterByCharacter),用于逐个字符显示完整文本。在流式场景下,你每次收到一小段 data.text 就直接追加到目标容器中,并继续触发逐字动画——但需要注意,追加的内容本身可能已经是一个词或一句话,如果你希望模拟“一个一个字符出现”的平滑效果,可以这样做:
维护一个全局的打字队列,每次收到新片段时,将片段加入队列。
由一个“打字机”定时器逐字从队列头部取出并显示。
或者更简单:直接实时追加(不做逐字延迟),因为流式本身已经是一块一块到达,视觉上已经接近打字效果。如果需要更细粒度,可用如下简单方法:
javascript let messageContainer = document.getElementById('chat-output'); let currentText = '';
function appendStreamChunk(chunkText) { // 将新收到的字符逐步添加到界面(如果需要逐个字符动画) for (let i = 0; i < chunkText.length; i++) { setTimeout(() => { messageContainer.textContent += chunkText[i]; }, i * 30); // 每个字符延迟 30ms } } 实际改进:可以使用 requestAnimationFrame 或更优雅的异步队列,但原理相似。
三、其他注意事项 超时设置:流式响应可能持续较长时间,需确保 Flask 和反向代理(如 nginx)的超时配置足够长。
错误处理:流式传输中如果 API 中断,需向前端发送错误消息并关闭连接。
原有非流式接口:可以保留原 /call_deepseek 作为非流式备用。
API 路由:建议新建 /call_deepseek_stream 路由,避免破坏已有前端调用。
CORS:如果前端独立部署,需要在 Flask 中配置 CORS 支持流式响应。
四、总结改动点 模块 改动内容 后端 - payload 中 stream: True - 使用 requests.post(..., stream=True) - 逐行解析 SSE 数据包 - 返回 Response(generate(), content_type='text/event-stream') 前端 - 使用 fetch + ReadableStream 读取块 - 解析 SSE 格式 - 实时将收到的文字片段累加到页面(可选逐字动画) 流式改造后,用户体验会更接近实时聊天,且首字延迟大大降低。如果有前端原有代码,我可以帮你具体适配。
<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>repokuanit.com dsapi逐字显示演示</title> <style> body { font-family: 'Arial', sans-serif; max-width: 800px; margin: 0 auto; padding: 20px; background-color: #f5f5f5; } .button-container { display: flex; flex-wrap: wrap; gap: 10px; margin-bottom: 20px; } button { padding: 10px 15px; background-color: #4CAF50; color: white; border: none; border-radius: 4px; cursor: pointer; font-size: 16px; transition: background-color 0.3s; } button:hover { background-color: #45a049; } .text-display { background-color: white; border: 1px solid #ddd; border-radius: 4px; padding: 15px; min-height: 200px; font-size: 16px; line-height: 1.6; white-space: pre-wrap; } .input-container { margin-top: 20px; } textarea { width: 100%; padding: 10px; border: 1px solid #ddd; border-radius: 4px; resize: vertical; min-height: 80px; font-size: 16px; } .send-btn { margin-top: 10px; background-color: #2196F3; } .send-btn:hover { background-color: #0b7dda; } .typing-cursor { display: inline-block; width: 8px; height: 16px; background-color: #333; animation: blink 1s infinite; vertical-align: middle; margin-left: 2px; } @keyframes blink { 0%, 100% { opacity: 1; } 50% { opacity: 0; } } </style> <script src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js"></script>
</head> <body> <h1>文本逐字显示演示</h1> <p>点击按钮查看逐字显示效果</p> <div class="button-container"> <button id="btn1">按钮1</button> <button id="btn2">按钮2</button> <button id="btn3">按钮3</button> <button id="btn4">按钮4</button> </div> <div class="text-display" id="textDisplay"></div> <div class="input-container"> <textarea id="userInput" placeholder="在这里输入文本..."></textarea> <button class="send-btn" id="sendBtn">发送</button> </div>
<script> document.addEventListener('DOMContentLoaded', function() { const buttons = document.querySelectorAll('button[id^="btn"]'); const textDisplay = document.getElementById('textDisplay'); const userInput = document.getElementById('userInput'); const sendBtn = document.getElementById('sendBtn'); // 为每个按钮添加点击事件 buttons.forEach(button => { button.addEventListener('click', function() { const buttonId = this.id; fetchResponse(buttonId); }); }); /* // 发送按钮点击事件 sendBtn.addEventListener('click', function() { const inputText = userInput.value.trim(); if (inputText) { // 显示用户输入 addToDisplay(`用户: ${inputText}`, 'user-message'); userInput.value = ''; // 这里可以添加处理用户输入的逻辑 // 模拟响应 setTimeout(() => { typeText(`这是一个对"${inputText}"的模拟响应。逐字显示效果可以用于聊天机器人等应用。`); }, 500); } }); */ // ... 其他代码保持不变 ...
// 发送按钮点击事件 sendBtn.addEventListener('click', function() { const inputText = userInput.value.trim(); if (inputText) { // 显示用户输入 addToDisplay(`用户: ${inputText}`, 'user-message'); userInput.value = ''; // 调用DeepSeek API fetch('/call_deepseek', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ user_input: inputText }) }) .then(response => response.json()) .then(data => { if (data.error) { typeText(`错误: ${data.error}`); } else { typeText(data.text); } }) .catch(error => { console.error('Error:', error); typeText("调用DeepSeek API时出错,请重试。"); }); } });
// ... 保持其他函数不变 ... // 从服务器获取响应 function fetchResponse(buttonId) { fetch('/get_response', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ button_id: buttonId }) }) .then(response => response.json()) .then(data => { typeText(data.text); }) .catch(error => { console.error('Error:', error); typeText("获取响应时出错,请重试。"); }); } // 逐字显示文本 function typeText(text, speed = 30) { // 清除之前的光标 const oldCursor = document.querySelector('.typing-cursor'); if (oldCursor) oldCursor.remove(); // 添加新行 const newLine = document.createElement('div'); newLine.className = 'response-message'; textDisplay.appendChild(newLine); let i = 0; const cursor = document.createElement('span'); cursor.className = 'typing-cursor'; newLine.appendChild(cursor); const interval = setInterval(() => { if (i < text.length) { // 替换最后一个字符和光标 newLine.innerHTML = text.substring(0, i + 1) + '<span class="typing-cursor"></span>'; i++; } else { clearInterval(interval); // 移除光标 newLine.innerHTML = text; } }, speed); // 滚动到底部 textDisplay.scrollTop = textDisplay.scrollHeight; } // 添加普通文本到显示区域 function addToDisplay(text, className) { const newLine = document.createElement('div'); newLine.className = className; newLine.textContent = text; textDisplay.appendChild(newLine); textDisplay.scrollTop = textDisplay.scrollHeight; } }); </script> <p></p><p></p><p></p><p></p><p></p><p></p> <p><a href="https://beian.miit.gov.cn/">皖ICP备2024070331号-1</a></p> <p>$$ \sum_{n=1}^{\infty} \frac{1}{n^2} = \frac{\pi^2}{6} $$</p> </body> </html>这是前端代码,如何修改,给出完整repo.py,和index.html 我们被问到:用户提供了一个前端index.html(已经包含了一些调用后端/get_response和/call_deepseek的逻辑),现在希望将后端改为流式(DeepSeek API流式),并且要求给出完整的repo.py和index.html。需要给出修改后的完整代码文件。
分析需求:原后端是Flask app,已有非流式/call_deepseek。现要改为流式,并提供完整的前端适配。用户的前端目前用的是fetch获取完整响应然后typeText。现在需要改成流式:前端应该收到流式数据块并实时追加显示。
我们需要提供两个文件:
repo.py(即后端的Python文件)
index.html(修改后的前端文件)
需要注意的是,原前端有一个逐字显示效果typeText,它是对整个文本进行逐字动画。但是在流式场景下,我们可能希望收到每个小块后直接追加文本(或者也做逐字效果)。通常会简化:直接将每个chunk追加到显示区域,不叠加额外的逐字动画,因为网络流本身已经是分块到达,看起来像打字效果。或者可以保留逐字效果,但比较复杂。建议直接即时显示每个chunk,可以不用原有的typeText的setInterval逐字显示,而是简单追加内容。不过可以保持一个近似效果:每次收到chunk,追加到当前正在显示的消息末尾,没有额外的光标动画。也可以实现光标闪烁等。
为了简洁和兼容,我们修改前端:创建新的流式处理函数,调用新的后端路由/call_deepseek_stream,使用fetch + ReadableStream解析SSE,每收到一个data块,就将其中的文本追加到当前消息div中,并滚动到底部。
后端:新建/call_deepseek_stream路由,返回Response(generate(), content_type='text/event-stream'),generate函数解析DeepSeek流式响应。
同时保留原有的非流式按钮功能吗?用户没有要求删除,可以保留。
注意原前端有按钮1-4使用的是/get_response(本地预定义文本),这不需要改动。
我们需要确保后端API密钥从环境变量读取,正确设置请求参数。用户原payload中有一个字段"thinking": {"type": "enabled"},"reasoning_effort": "high",这些似乎是DeepSeek的特定参数,我们保留。
另外,模型名称用户使用"deepseek-v4-pro",可能不规范,但保留原样。
提供完整代码,包括导入和运行。
还要注意,Flask处理流式响应时,可能会遇到缓冲区问题,需要设置response直接返回生成器,不要用jsonify。
我们还要确保前端正确处理SSE格式:后端yield的格式是"data: {json}\n\n",前端解析时按行分割,注意data:前缀和后缀。
前端需要停止旧的逐字显示函数,新的显示方式:每次收到文本片段,直接追加到当前显示的消息内容中。同时,考虑到可能有多个对话轮次,每次发送新消息时创建一个新的div作为回复容器,然后流式追加。这是标准聊天界面的做法。
此外,原前端有一个addToDisplay用于显示用户消息,可以保留。然后创建一个全局变量currentResponseDiv指向当前正在接收的回复div。
写代码时要注意错误处理,流式中断、超时等。
最终输出:先给repo.py内容,再给index.html内容,用合适的标记分隔。
开始编写。
我们提供一个完整的流式对话版本:后端 Flask 支持 SSE 流式推送,前端使用 fetch + ReadableStream 实时接收并逐字显示(无额外延迟,自然形成打字效果)。
1. repo.py(后端) python from flask import Flask, render_template, request, Response, jsonify import os import requests import json
app = Flask(__name__)
# 预定义的按钮响应(非流式,保留原有功能) responses = { "btn1": "你好!欢迎使用这个文本逐字显示演示。这是按钮1的响应内容。", "btn2": "这是按钮2的响应内容。逐字显示效果可以让信息呈现更加生动有趣。", "btn3": "按钮3被点击了!这个功能可以用来创建聊天机器人或者交互式故事。", "btn4": "感谢您测试这个功能。逐字显示模拟了人类打字的效果,增强了用户体验。" }
# DeepSeek API 配置 DEEPSEEK_API_URL = "https://api.deepseek.com/chat/completions" DEEPSEEK_API_KEY = os.getenv('DEEPSEEK_API_KEY') # 从环境变量读取
@app.route('/') def index(): return render_template('index.html')
# 原有非流式按钮响应(预定义文本) @app.route('/get_response', methods=['POST']) def get_response(): data = request.json button_id = data.get('button_id') response_text = responses.get(button_id, "未知按钮") return jsonify({"text": response_text})
# 新增流式 DeepSeek 接口 @app.route('/call_deepseek_stream', methods=['POST']) def call_deepseek_stream(): user_input = request.json.get('user_input') if not user_input: return jsonify({"error": "No input provided"}), 400
def generate(): headers = { "Authorization": f"Bearer {DEEPSEEK_API_KEY}", "Content-Type": "application/json" } payload = { "model": "deepseek-v4-pro", # 用户原参数 "messages": [{"role": "user", "content": user_input}], "stream": True, "thinking": {"type": "enabled"}, "reasoning_effort": "high" } try: # 发送流式请求 response = requests.post(DEEPSEEK_API_URL, json=payload, headers=headers, stream=True) response.raise_for_status()
# 逐行读取 SSE 数据 for line in response.iter_lines(decode_unicode=True): if not line: continue # 移除 "data: " 前缀 if line.startswith('data: '): data_str = line[6:] else: data_str = line
if data_str == '[DONE]': break
try: chunk = json.loads(data_str) delta = chunk['choices'][0].get('delta', {}) content = delta.get('content', '') if content: # 向前端发送 SSE 格式数据 yield f"data: {json.dumps({'text': content})}\n\n" except json.JSONDecodeError: continue except Exception as e: yield f"data: {json.dumps({'error': str(e)})}\n\n"
return Response(generate(), content_type='text/event-stream')
if __name__ == '__main__': app.run(debug=True) 2. index.html(前端) 完整的前端代码,支持流式接收并实时显示,同时保留原有的按钮预定义文本功能。
html <!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>流式对话演示 - DeepSeek</title> <style> body { font-family: 'Arial', sans-serif; max-width: 800px; margin: 0 auto; padding: 20px; background-color: #f5f5f5; } .button-container { display: flex; flex-wrap: wrap; gap: 10px; margin-bottom: 20px; } button { padding: 10px 15px; background-color: #4CAF50; color: white; border: none; border-radius: 4px; cursor: pointer; font-size: 16px; transition: background-color 0.3s; } button:hover { background-color: #45a049; } .text-display { background-color: white; border: 1px solid #ddd; border-radius: 4px; padding: 15px; min-height: 300px; max-height: 500px; overflow-y: auto; font-size: 16px; line-height: 1.6; white-space: pre-wrap; } .user-message { background-color: #e3f2fd; padding: 8px 12px; border-radius: 12px; margin: 8px 0; text-align: right; } .response-message { background-color: #f1f1f1; padding: 8px 12px; border-radius: 12px; margin: 8px 0; text-align: left; } .input-container { margin-top: 20px; display: flex; gap: 10px; align-items: flex-start; } textarea { flex: 1; padding: 10px; border: 1px solid #ddd; border-radius: 4px; resize: vertical; min-height: 80px; font-size: 16px; } .send-btn { background-color: #2196F3; padding: 10px 20px; height: 50px; align-self: center; } .send-btn:hover { background-color: #0b7dda; } .typing-cursor { display: inline-block; width: 8px; height: 16px; background-color: #333; animation: blink 1s infinite; vertical-align: middle; margin-left: 2px; } @keyframes blink { 0%, 100% { opacity: 1; } 50% { opacity: 0; } } </style> <script src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js"></script> </head> <body> <h1>逐字显示 + DeepSeek 流式对话</h1> <p>点击下方按钮(预定义文本)或输入内容后发送(流式 AI 回复)</p>
<div class="button-container"> <button id="btn1">按钮1</button> <button id="btn2">按钮2</button> <button id="btn3">按钮3</button> <button id="btn4">按钮4</button> </div>
<div class="text-display" id="textDisplay"></div>
<div class="input-container"> <textarea id="userInput" placeholder="在这里输入文本,将调用 DeepSeek 流式 API..."></textarea> <button class="send-btn" id="sendBtn">发送</button> </div>
<p></p> <p><a href="https://beian.miit.gov.cn/">皖ICP备2024070331号-1</a></p> <p>$$ \sum_{n=1}^{\infty} \frac{1}{n^2} = \frac{\pi^2}{6} $$</p>
<script> document.addEventListener('DOMContentLoaded', function() { const textDisplay = document.getElementById('textDisplay'); const userInput = document.getElementById('userInput'); const sendBtn = document.getElementById('sendBtn'); // 全局变量:用于流式存储当前正在接收的回复元素 let currentResponseDiv = null;
// ========== 按钮预定义文本(非流式,保持原有效果) ========== const buttons = document.querySelectorAll('button[id^="btn"]'); buttons.forEach(button => { button.addEventListener('click', function() { const buttonId = this.id; fetch('/get_response', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ button_id: buttonId }) }) .then(response => response.json()) .then(data => { typeText(data.text); // 使用原有逐字动画 }) .catch(error => { console.error('Error:', error); typeText("获取响应时出错,请重试。"); }); }); });
// ========== 原有的逐字显示函数(用于非流式) ========== function typeText(text, speed = 30) { // 创建新的消息容器 const newLine = document.createElement('div'); newLine.className = 'response-message'; textDisplay.appendChild(newLine); let i = 0; const cursor = document.createElement('span'); cursor.className = 'typing-cursor'; newLine.appendChild(cursor); const interval = setInterval(() => { if (i < text.length) { newLine.innerHTML = text.substring(0, i + 1) + '<span class="typing-cursor"></span>'; i++; textDisplay.scrollTop = textDisplay.scrollHeight; } else { clearInterval(interval); newLine.innerHTML = text; textDisplay.scrollTop = textDisplay.scrollHeight; } }, speed); }
// ========== 流式 DeepSeek 对话 ========== sendBtn.addEventListener('click', async function() { const inputText = userInput.value.trim(); if (!inputText) return; // 显示用户消息 addToDisplay(`用户: ${inputText}`, 'user-message'); userInput.value = ''; // 准备接收 AI 回复的容器 currentResponseDiv = document.createElement('div'); currentResponseDiv.className = 'response-message'; currentResponseDiv.innerHTML = ''; // 初始为空 textDisplay.appendChild(currentResponseDiv); // 可选:显示光标(模拟等待) const cursorSpan = document.createElement('span'); cursorSpan.className = 'typing-cursor'; currentResponseDiv.appendChild(cursorSpan); try { const response = await fetch('/call_deepseek_stream', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ user_input: inputText }) }); if (!response.ok) throw new Error(`HTTP ${response.status}`); const reader = response.body.getReader(); const decoder = new TextDecoder('utf-8'); let buffer = ''; let fullText = ''; while (true) { const { done, value } = await reader.read(); if (done) break; buffer += decoder.decode(value, { stream: true }); const lines = buffer.split('\n\n'); buffer = lines.pop(); // 保留不完整的行 for (const line of lines) { if (line.startsWith('data: ')) { const dataStr = line.slice(6); if (dataStr === '[DONE]') continue; try { const data = JSON.parse(dataStr); if (data.error) { currentResponseDiv.innerHTML = `错误: ${data.error}`; break; } if (data.text) { fullText += data.text; // 实时追加文本,并移除光标再重新添加(模拟打字效果) currentResponseDiv.innerHTML = fullText + '<span class="typing-cursor"></span>'; textDisplay.scrollTop = textDisplay.scrollHeight; } } catch(e) { console.warn('解析错误', e); } } } } // 流结束,移除光标,保留最终文本 if (currentResponseDiv) { currentResponseDiv.innerHTML = fullText; } } catch (error) { console.error('流式请求失败:', error); if (currentResponseDiv) { currentResponseDiv.innerHTML = '网络错误或 API 调用失败,请重试。'; } else { addToDisplay('网络错误或 API 调用失败,请重试。', 'response-message'); } } finally { currentResponseDiv = null; } }); // 辅助函数:直接添加文本(用于用户消息和普通消息) function addToDisplay(text, className) { const div = document.createElement('div'); div.className = className; div.textContent = text; textDisplay.appendChild(div); textDisplay.scrollTop = textDisplay.scrollHeight; } }); </script> </body> </html> 使用说明 设置环境变量
bash export DEEPSEEK_API_KEY="你的真实 API Key" 安装依赖
bash pip install flask requests 运行后端
bash python repo.py 访问 http://127.0.0.1:5000,即可测试:
点击四个按钮 → 预定义文本(非流式逐字动画)
在文本框输入内容并点击“发送” → 调用 DeepSeek 流式 API,实时显示回复内容
主要改进点 模块 改动说明 后端 新增 /call_deepseek_stream 路由,使用 stream=True 请求 API,逐行解析 SSE 并 yield 给前端 前端 使用 fetch + ReadableStream 接收流式数据;每收到一个 data.text 就立即追加到当前回复容器中,自然形成打字效果 用户体验 保留原有按钮的逐字动画;流式消息不再额外添加延迟,网络到达速度即显示速度,首字响应更快 如果你希望流式消息也带有“逐字符延迟”的平滑效果,可以在前端收到 data.text 后,用 setTimeout 逐个字符追加。不过对于大多数场景,流式本身的块状到达已经足够自然。如有需要,我可以进一步调整代码。
|