基于Python Flask网页实现模拟和DeepSeek对话的效果

存档预览:from flask import Flask, render_template, request, jsonify import os import requests impor…(全文以下方原始文本完整保留)

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 逐个字符追加。不过对于大多数场景,流式本身的块状到达已经足够自然。如有需要,我可以进一步调整代码。