samlax12 commited on
Commit
cad2e0b
·
verified ·
1 Parent(s): 77a12c8

Create student.html

Browse files
Files changed (1) hide show
  1. templates/student.html +1651 -0
templates/student.html ADDED
@@ -0,0 +1,1651 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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>AI学习助手</title>
7
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css">
8
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/font/bootstrap-icons.css">
9
+ <!-- KaTeX 用于渲染数学公式 -->
10
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/dist/katex.min.css">
11
+ <!-- Highlight.js 样式 -->
12
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.8.0/styles/github.min.css">
13
+ <style>
14
+ @import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap');
15
+
16
+ :root {
17
+ /* 主要配色 */
18
+ --primary-color: #0f2d49;
19
+ --primary-light: #234a70;
20
+ --secondary-color: #4a6cfd;
21
+ --secondary-light: #7b91ff;
22
+ --tertiary-color: #f7f9fe;
23
+
24
+ /* 功能颜色 */
25
+ --success-color: #10b981;
26
+ --success-light: #d1fae5;
27
+ --warning-color: #f59e0b;
28
+ --warning-light: #fef3c7;
29
+ --danger-color: #ef4444;
30
+ --danger-light: #fee2e2;
31
+ --info-color: #3b82f6;
32
+ --info-light: #dbeafe;
33
+
34
+ /* 中性色 */
35
+ --neutral-50: #f9fafb;
36
+ --neutral-100: #f3f4f6;
37
+ --neutral-200: #e5e7eb;
38
+ --neutral-300: #d1d5db;
39
+ --neutral-400: #9ca3af;
40
+ --neutral-500: #6b7280;
41
+ --neutral-600: #4b5563;
42
+ --neutral-700: #374151;
43
+ --neutral-800: #1f2937;
44
+ --neutral-900: #111827;
45
+
46
+ /* 界面元素 */
47
+ --border-radius-sm: 0.25rem;
48
+ --border-radius: 0.375rem;
49
+ --border-radius-lg: 0.5rem;
50
+ --border-radius-xl: 0.75rem;
51
+ --border-radius-xxl: 1rem;
52
+ --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.05);
53
+ --shadow: 0 1px 3px rgba(0, 0, 0, 0.1), 0 1px 2px rgba(0, 0, 0, 0.06);
54
+ --shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
55
+ --shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
56
+ --shadow-inner: inset 0 2px 4px 0 rgba(0, 0, 0, 0.06);
57
+ --transition-base: all 0.2s ease-in-out;
58
+ --transition-smooth: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
59
+ --font-family: 'Inter', 'PingFang SC', 'Microsoft YaHei', sans-serif;
60
+ }
61
+
62
+ body {
63
+ font-family: var(--font-family);
64
+ margin: 0;
65
+ padding: 0;
66
+ height: 100vh;
67
+ background-color: var(--neutral-50);
68
+ color: var(--neutral-800);
69
+ display: flex;
70
+ flex-direction: column;
71
+ -webkit-font-smoothing: antialiased;
72
+ -moz-osx-font-smoothing: grayscale;
73
+ }
74
+
75
+ /* 头部样式 */
76
+ .header {
77
+ background: linear-gradient(135deg, var(--primary-color), var(--primary-light));
78
+ border-bottom: 1px solid rgba(255, 255, 255, 0.1);
79
+ color: white;
80
+ padding: 1rem 1.5rem;
81
+ box-shadow: var(--shadow-md);
82
+ position: relative;
83
+ z-index: 10;
84
+ }
85
+
86
+ .header-content {
87
+ max-width: 1600px;
88
+ margin: 0 auto;
89
+ display: flex;
90
+ justify-content: space-between;
91
+ align-items: center;
92
+ }
93
+
94
+ .header h1 {
95
+ margin: 0;
96
+ font-size: 1.5rem;
97
+ font-weight: 600;
98
+ color: white;
99
+ display: flex;
100
+ align-items: center;
101
+ }
102
+
103
+ .header h1 i {
104
+ margin-right: 0.75rem;
105
+ color: rgba(255, 255, 255, 0.8);
106
+ }
107
+
108
+ .header .badge {
109
+ background-color: rgba(255, 255, 255, 0.15);
110
+ color: white;
111
+ font-weight: 500;
112
+ padding: 0.35em 0.75em;
113
+ font-size: 0.85rem;
114
+ border-radius: 9999px;
115
+ }
116
+
117
+ /* 主容器样式 */
118
+ .main-container {
119
+ flex: 1;
120
+ display: flex;
121
+ max-width: 1600px;
122
+ margin: 0 auto;
123
+ padding: 1.5rem;
124
+ width: 100%;
125
+ box-sizing: border-box;
126
+ height: calc(100vh - 70px); /* Header height + padding */
127
+ overflow: hidden;
128
+ }
129
+
130
+ .chat-container {
131
+ flex: 1;
132
+ display: flex;
133
+ flex-direction: column;
134
+ background-color: white;
135
+ border-radius: var(--border-radius-xl);
136
+ box-shadow: var(--shadow);
137
+ overflow: hidden;
138
+ height: 100%;
139
+ max-width: 800px;
140
+ margin: 0 auto;
141
+ transition: var(--transition-smooth);
142
+ position: relative;
143
+ }
144
+
145
+ /* 聊天对话区域样式 */
146
+ .chat-messages {
147
+ flex: 1;
148
+ overflow-y: auto;
149
+ padding: 1.5rem;
150
+ display: flex;
151
+ flex-direction: column;
152
+ background-color: var(--neutral-50);
153
+ gap: 1rem;
154
+ scroll-behavior: smooth;
155
+ }
156
+
157
+ /* 自定义滚动条 */
158
+ .chat-messages::-webkit-scrollbar {
159
+ width: 6px;
160
+ height: 6px;
161
+ }
162
+
163
+ .chat-messages::-webkit-scrollbar-track {
164
+ background: var(--neutral-100);
165
+ border-radius: 10px;
166
+ }
167
+
168
+ .chat-messages::-webkit-scrollbar-thumb {
169
+ background: var(--neutral-300);
170
+ border-radius: 10px;
171
+ }
172
+
173
+ .chat-messages::-webkit-scrollbar-thumb:hover {
174
+ background: var(--neutral-400);
175
+ }
176
+
177
+ /* 消息气泡样式 */
178
+ .message {
179
+ max-width: 85%;
180
+ border-radius: var(--border-radius-lg);
181
+ padding: 1rem;
182
+ position: relative;
183
+ line-height: 1.5;
184
+ box-shadow: var(--shadow-sm);
185
+ overflow-wrap: break-word;
186
+ word-wrap: break-word;
187
+ hyphens: auto;
188
+ }
189
+
190
+ .message .message-content {
191
+ padding: 0 10px;
192
+ margin: 0;
193
+ }
194
+
195
+ .user-message {
196
+ background: linear-gradient(135deg, #e9f5ff, #c2e4ff);
197
+ align-self: flex-end;
198
+ color: var(--primary-color);
199
+ border-bottom-right-radius: 0.2rem;
200
+ border-left: 1px solid rgba(74, 108, 253, 0.1);
201
+ border-top: 1px solid rgba(255, 255, 255, 0.5);
202
+ }
203
+
204
+ .bot-message {
205
+ background-color: white;
206
+ align-self: flex-start;
207
+ color: var(--neutral-800);
208
+ border-bottom-left-radius: 0.2rem;
209
+ border-left: 3px solid var(--secondary-color);
210
+ box-shadow: var(--shadow);
211
+ }
212
+
213
+ /* 输入区域样式 */
214
+ .input-container {
215
+ padding: 1rem 1.5rem;
216
+ border-top: 1px solid var(--neutral-200);
217
+ background-color: white;
218
+ position: relative;
219
+ }
220
+
221
+ .input-row {
222
+ display: flex;
223
+ gap: 0.75rem;
224
+ position: relative;
225
+ }
226
+
227
+ .input-field {
228
+ flex: 1;
229
+ padding: 0.85rem 1rem;
230
+ border: 1px solid var(--neutral-200);
231
+ border-radius: var(--border-radius-lg);
232
+ resize: none;
233
+ font-size: 0.95rem;
234
+ box-shadow: var(--shadow-inner);
235
+ transition: var(--transition-base);
236
+ }
237
+
238
+ .input-field:focus {
239
+ outline: none;
240
+ border-color: var(--secondary-color);
241
+ box-shadow: 0 0 0 3px rgba(74, 108, 253, 0.15);
242
+ }
243
+
244
+ .send-button {
245
+ padding: 0 1.25rem;
246
+ background: linear-gradient(135deg, var(--secondary-color), var(--secondary-light));
247
+ color: white;
248
+ border: none;
249
+ border-radius: var(--border-radius-lg);
250
+ cursor: pointer;
251
+ font-weight: 500;
252
+ transition: var(--transition-base);
253
+ display: flex;
254
+ align-items: center;
255
+ justify-content: center;
256
+ }
257
+
258
+ .send-button:hover {
259
+ box-shadow: 0 4px 10px rgba(74, 108, 253, 0.25);
260
+ transform: translateY(-1px);
261
+ }
262
+
263
+ .send-button:disabled {
264
+ background: var(--neutral-300);
265
+ cursor: not-allowed;
266
+ transform: none;
267
+ box-shadow: none;
268
+ }
269
+
270
+ /* 插件容器样式 */
271
+ .plugin-container {
272
+ display: none;
273
+ flex-direction: column;
274
+ background-color: white;
275
+ border-radius: var(--border-radius-xl);
276
+ box-shadow: var(--shadow);
277
+ overflow: hidden;
278
+ height: 100%;
279
+ flex: 2;
280
+ margin-left: 1.5rem;
281
+ transition: var(--transition-smooth);
282
+ }
283
+
284
+ .plugin-header {
285
+ padding: 0.85rem 1.25rem;
286
+ border-bottom: 1px solid var(--neutral-200);
287
+ background-color: var(--neutral-50);
288
+ display: flex;
289
+ justify-content: space-between;
290
+ align-items: center;
291
+ }
292
+
293
+ .plugin-title {
294
+ margin: 0;
295
+ font-size: 1rem;
296
+ font-weight: 600;
297
+ color: var(--primary-color);
298
+ display: flex;
299
+ align-items: center;
300
+ }
301
+
302
+ .plugin-title i {
303
+ margin-right: 0.5rem;
304
+ color: var(--secondary-color);
305
+ }
306
+
307
+ .plugin-close {
308
+ width: 32px;
309
+ height: 32px;
310
+ background: var(--neutral-100);
311
+ border: none;
312
+ border-radius: 50%;
313
+ cursor: pointer;
314
+ font-size: 0.85rem;
315
+ color: var(--neutral-700);
316
+ display: flex;
317
+ align-items: center;
318
+ justify-content: center;
319
+ transition: var(--transition-base);
320
+ }
321
+
322
+ .plugin-close:hover {
323
+ background-color: var(--neutral-200);
324
+ color: var(--neutral-900);
325
+ }
326
+
327
+ .plugin-content {
328
+ flex: 1;
329
+ overflow-y: auto;
330
+ padding: 0;
331
+ background-color: var(--neutral-50);
332
+ }
333
+
334
+ /* 当插件激活时的样式 */
335
+ .main-container.with-plugin .chat-container {
336
+ max-width: 380px;
337
+ margin: 0;
338
+ }
339
+
340
+ /* 代码块样式 */
341
+ pre {
342
+ margin: 1rem 0;
343
+ padding: 1rem;
344
+ background-color: var(--neutral-800);
345
+ border-radius: var(--border-radius);
346
+ overflow-x: auto;
347
+ position: relative;
348
+ }
349
+
350
+ pre code {
351
+ font-family: 'JetBrains Mono', 'Fira Code', Consolas, monospace;
352
+ font-size: 0.9rem;
353
+ color: #e5e7eb;
354
+ padding: 0;
355
+ background: none;
356
+ }
357
+
358
+ .copy-button {
359
+ position: absolute;
360
+ top: 0.5rem;
361
+ right: 0.5rem;
362
+ padding: 0.25rem 0.5rem;
363
+ background-color: rgba(255, 255, 255, 0.1);
364
+ color: rgba(255, 255, 255, 0.6);
365
+ border: none;
366
+ border-radius: var(--border-radius-sm);
367
+ font-size: 0.75rem;
368
+ cursor: pointer;
369
+ transition: var(--transition-base);
370
+ display: flex;
371
+ align-items: center;
372
+ gap: 0.25rem;
373
+ }
374
+
375
+ .copy-button:hover {
376
+ background-color: rgba(255, 255, 255, 0.2);
377
+ color: rgba(255, 255, 255, 0.9);
378
+ }
379
+
380
+ /* 代码执行结果样式 */
381
+ .code-result {
382
+ margin-top: 0.5rem;
383
+ padding: 0.75rem;
384
+ background-color: var(--neutral-900);
385
+ border-radius: var(--border-radius);
386
+ color: white;
387
+ font-family: 'JetBrains Mono', 'Fira Code', Consolas, monospace;
388
+ font-size: 0.85rem;
389
+ white-space: pre-wrap;
390
+ }
391
+
392
+ /* 内联代码样式 */
393
+ code:not(pre code) {
394
+ font-family: 'JetBrains Mono', 'Fira Code', Consolas, monospace;
395
+ font-size: 0.9em;
396
+ color: var(--primary-color);
397
+ background-color: var(--neutral-100);
398
+ padding: 0.2em 0.4em;
399
+ border-radius: 3px;
400
+ }
401
+
402
+ /* 参考资料样式 */
403
+ .reference-container {
404
+ margin-top: 1rem;
405
+ border-top: 1px dashed var(--neutral-300);
406
+ padding-top: 0.75rem;
407
+ }
408
+
409
+ .reference-toggle {
410
+ color: var(--neutral-600);
411
+ font-size: 0.85rem;
412
+ font-weight: 500;
413
+ cursor: pointer;
414
+ display: flex;
415
+ align-items: center;
416
+ transition: var(--transition-base);
417
+ }
418
+
419
+ .reference-toggle:hover {
420
+ color: var(--secondary-color);
421
+ }
422
+
423
+ .reference-toggle i {
424
+ margin-right: 0.35rem;
425
+ font-size: 0.9rem;
426
+ }
427
+
428
+ .reference-content {
429
+ display: none;
430
+ margin-top: 0.75rem;
431
+ padding: 0.75rem;
432
+ background-color: var(--neutral-100);
433
+ border-radius: var(--border-radius);
434
+ font-size: 0.85rem;
435
+ }
436
+
437
+ .reference-item {
438
+ margin-bottom: 0.75rem;
439
+ padding-bottom: 0.75rem;
440
+ border-bottom: 1px solid var(--neutral-200);
441
+ }
442
+
443
+ .reference-item:last-child {
444
+ margin-bottom: 0;
445
+ padding-bottom: 0;
446
+ border-bottom: none;
447
+ }
448
+
449
+ .reference-item-source {
450
+ color: var(--neutral-600);
451
+ font-size: 0.8rem;
452
+ margin-top: 0.25rem;
453
+ }
454
+
455
+ /* 欢迎消息样式 */
456
+ .welcome-message {
457
+ display: flex;
458
+ flex-direction: column;
459
+ align-items: center;
460
+ text-align: center;
461
+ margin-bottom: 1.5rem;
462
+ }
463
+
464
+ .welcome-icon {
465
+ width: 64px;
466
+ height: 64px;
467
+ background: linear-gradient(135deg, var(--secondary-color), var(--secondary-light));
468
+ border-radius: 50%;
469
+ display: flex;
470
+ align-items: center;
471
+ justify-content: center;
472
+ margin-bottom: 1rem;
473
+ color: white;
474
+ font-size: 2rem;
475
+ box-shadow: 0 4px 12px rgba(74, 108, 253, 0.25);
476
+ }
477
+
478
+ .welcome-title {
479
+ font-size: 1.5rem;
480
+ font-weight: 600;
481
+ margin-bottom: 0.5rem;
482
+ color: var(--primary-color);
483
+ }
484
+
485
+ .welcome-description {
486
+ color: var(--neutral-700);
487
+ max-width: 400px;
488
+ margin-bottom: 0;
489
+ }
490
+
491
+ /* 思维导图和可视化插件内容样式 */
492
+ .visualization-result, .mindmap-result {
493
+ padding: 1.5rem;
494
+ text-align: center;
495
+ }
496
+
497
+ .visualization-image, .mindmap-image {
498
+ max-width: 100%;
499
+ border-radius: var(--border-radius);
500
+ box-shadow: var(--shadow);
501
+ }
502
+
503
+ /* 响应式样式 */
504
+ @media (max-width: 1200px) {
505
+ .main-container.with-plugin .chat-container {
506
+ max-width: 320px;
507
+ }
508
+ }
509
+
510
+ @media (max-width: 992px) {
511
+ .main-container {
512
+ flex-direction: column;
513
+ padding: 1rem;
514
+ }
515
+
516
+ .main-container.with-plugin .chat-container {
517
+ max-width: 100%;
518
+ margin-bottom: 1rem;
519
+ height: 40vh;
520
+ }
521
+
522
+ .plugin-container {
523
+ margin-left: 0;
524
+ height: calc(60vh - 32px);
525
+ }
526
+ }
527
+
528
+ @media (max-width: 768px) {
529
+ .main-container {
530
+ padding: 0.75rem;
531
+ }
532
+
533
+ .message {
534
+ max-width: 95%;
535
+ }
536
+ }
537
+
538
+ /* 增强的卡塔赫和Markdown样式 */
539
+ .katex {
540
+ font-size: 1.1em;
541
+ }
542
+
543
+ .message h1, .message h2, .message h3,
544
+ .message h4, .message h5, .message h6 {
545
+ margin-top: 1em;
546
+ margin-bottom: 0.5em;
547
+ line-height: 1.3;
548
+ }
549
+
550
+ .message h1 {
551
+ font-size: 1.6em;
552
+ border-bottom: 1px solid var(--neutral-200);
553
+ padding-bottom: 0.3em;
554
+ }
555
+
556
+ .message h2 {
557
+ font-size: 1.4em;
558
+ border-bottom: 1px solid var(--neutral-200);
559
+ padding-bottom: 0.3em;
560
+ }
561
+
562
+ .message h3 {
563
+ font-size: 1.2em;
564
+ }
565
+
566
+ .message h4 {
567
+ font-size: 1.1em;
568
+ }
569
+
570
+ .message h5, .message h6 {
571
+ font-size: 1em;
572
+ }
573
+
574
+ .message p {
575
+ margin: 0.5em 0;
576
+ }
577
+
578
+ .message ul, .message ol {
579
+ margin: 0.5em 0;
580
+ padding-left: 1.5em;
581
+ }
582
+
583
+ .message li {
584
+ margin: 0.25em 0;
585
+ }
586
+
587
+ .message blockquote {
588
+ margin: 0.5em 0;
589
+ padding-left: 1em;
590
+ border-left: 4px solid var(--neutral-300);
591
+ color: var(--neutral-700);
592
+ }
593
+
594
+ .message img {
595
+ max-width: 100%;
596
+ border-radius: var(--border-radius);
597
+ }
598
+
599
+ .message table {
600
+ border-collapse: collapse;
601
+ margin: 1em 0;
602
+ width: 100%;
603
+ }
604
+
605
+ .message table th,
606
+ .message table td {
607
+ border: 1px solid var(--neutral-300);
608
+ padding: 0.5em;
609
+ }
610
+
611
+ .message table th {
612
+ background-color: var(--neutral-100);
613
+ font-weight: 600;
614
+ }
615
+
616
+ .message table tr:nth-child(even) {
617
+ background-color: var(--neutral-50);
618
+ }
619
+
620
+ /* 动画效果 */
621
+ @keyframes fadeIn {
622
+ from { opacity: 0; transform: translateY(10px); }
623
+ to { opacity: 1; transform: translateY(0); }
624
+ }
625
+
626
+ .message {
627
+ animation: fadeIn 0.3s ease forwards;
628
+ }
629
+
630
+ /* 输入提示区域 */
631
+ .input-suggestions {
632
+ position: absolute;
633
+ bottom: 100%;
634
+ left: 0;
635
+ right: 0;
636
+ background-color: white;
637
+ border-top-left-radius: var(--border-radius-lg);
638
+ border-top-right-radius: var(--border-radius-lg);
639
+ box-shadow: var(--shadow-md);
640
+ padding: 0.75rem;
641
+ display: none;
642
+ border: 1px solid var(--neutral-200);
643
+ border-bottom: none;
644
+ }
645
+
646
+ .suggestion-title {
647
+ font-size: 0.85rem;
648
+ font-weight: 600;
649
+ color: var(--neutral-700);
650
+ margin-bottom: 0.5rem;
651
+ }
652
+
653
+ .suggestion-buttons {
654
+ display: flex;
655
+ flex-wrap: wrap;
656
+ gap: 0.5rem;
657
+ }
658
+
659
+ .suggestion-button {
660
+ padding: 0.5rem 0.75rem;
661
+ background-color: var(--neutral-100);
662
+ border: 1px solid var(--neutral-200);
663
+ border-radius: var(--border-radius);
664
+ font-size: 0.85rem;
665
+ color: var(--neutral-800);
666
+ cursor: pointer;
667
+ transition: var(--transition-base);
668
+ }
669
+
670
+ .suggestion-button:hover {
671
+ background-color: var(--secondary-light);
672
+ color: white;
673
+ border-color: var(--secondary-light);
674
+ }
675
+
676
+ /* 打字机效果 */
677
+ .typing-indicator {
678
+ display: inline-block;
679
+ margin-left: 5px;
680
+ }
681
+
682
+ .typing-dot {
683
+ display: inline-block;
684
+ width: 6px;
685
+ height: 6px;
686
+ border-radius: 50%;
687
+ background-color: var(--neutral-500);
688
+ margin-right: 3px;
689
+ animation: typingDot 1.4s infinite ease-in-out;
690
+ }
691
+
692
+ .typing-dot:nth-child(1) { animation-delay: 0s; }
693
+ .typing-dot:nth-child(2) { animation-delay: 0.2s; }
694
+ .typing-dot:nth-child(3) { animation-delay: 0.4s; }
695
+
696
+ @keyframes typingDot {
697
+ 0%, 60%, 100% { transform: translateY(0); }
698
+ 30% { transform: translateY(-5px); }
699
+ }
700
+
701
+ /* 打字机控制样式 */
702
+ .typewriter-controls {
703
+ display: flex;
704
+ gap: 8px;
705
+ margin-top: 12px;
706
+ align-items: center;
707
+ justify-content: flex-end;
708
+ }
709
+
710
+ .typewriter-btn {
711
+ width: 32px;
712
+ height: 32px;
713
+ border-radius: 50%;
714
+ border: none;
715
+ background-color: var(--neutral-100);
716
+ color: var(--neutral-600);
717
+ cursor: pointer;
718
+ display: flex;
719
+ align-items: center;
720
+ justify-content: center;
721
+ transition: var(--transition-base);
722
+ }
723
+
724
+ .typewriter-btn:hover {
725
+ background-color: var(--neutral-200);
726
+ color: var(--neutral-900);
727
+ }
728
+
729
+ .typewriter-btn.pause-btn {
730
+ color: var(--danger-color);
731
+ }
732
+
733
+ .typewriter-btn.continue-btn {
734
+ color: var(--success-color);
735
+ }
736
+
737
+ .typewriter-btn.speed-btn {
738
+ color: var(--warning-color);
739
+ }
740
+
741
+ /* 打字机光标效果 */
742
+ .typing-cursor {
743
+ display: inline-block;
744
+ width: 2px;
745
+ height: 1em;
746
+ background-color: var(--neutral-700);
747
+ margin-left: 2px;
748
+ vertical-align: middle;
749
+ animation: blink 1s infinite;
750
+ }
751
+
752
+ @keyframes blink {
753
+ 0%, 100% { opacity: 1; }
754
+ 50% { opacity: 0; }
755
+ }
756
+
757
+ /* 确保代码块内文本溢出有滚动条 */
758
+ pre {
759
+ position: relative;
760
+ white-space: pre;
761
+ word-wrap: normal;
762
+ overflow-x: auto;
763
+ }
764
+
765
+ /* 保证消息区内容正常显示 */
766
+ .message-content p {
767
+ word-break: break-word;
768
+ }
769
+
770
+ /* 确保代码复制按钮正确显示 */
771
+ .copy-button {
772
+ opacity: 0.8;
773
+ z-index: 10;
774
+ }
775
+ </style>
776
+ </head>
777
+ <body>
778
+ <header class="header">
779
+ <div class="header-content">
780
+ <h1><i class="bi bi-mortarboard"></i> {{ agent_name }}</h1>
781
+ <div>
782
+ <span class="badge">AI学习助手</span>
783
+ </div>
784
+ </div>
785
+ </header>
786
+
787
+ <div class="main-container" id="main-container">
788
+ <div class="chat-container">
789
+ <div class="chat-messages" id="chat-messages">
790
+ <div class="welcome-message">
791
+ <div class="welcome-icon">
792
+ <i class="bi bi-robot"></i>
793
+ </div>
794
+ <h2 class="welcome-title">欢迎使用 {{ agent_name }}</h2>
795
+ {% if agent_description %}
796
+ <p class="welcome-description">{{ agent_description }}</p>
797
+ {% else %}
798
+ <p class="welcome-description">我是您的AI学习助手,有任何问题都可以随时向我提问</p>
799
+ {% endif %}
800
+ </div>
801
+
802
+ <div class="message bot-message">
803
+ <div class="message-content">
804
+ <p>您好!我是{{ agent_name }},很高兴能够帮助您学习。请问有什么我可以协助您的问题吗?</p>
805
+ </div>
806
+ </div>
807
+ </div>
808
+
809
+ <div class="input-container">
810
+ <div class="input-suggestions" id="input-suggestions">
811
+ <div class="suggestion-title">推荐问题:</div>
812
+ <div class="suggestion-buttons">
813
+ <button class="suggestion-button">介绍一下这门课程的主要内容</button>
814
+ <button class="suggestion-button">这门课程有哪些重点知识?</button>
815
+ <button class="suggestion-button">请给我���些学习建议</button>
816
+ </div>
817
+ </div>
818
+ <div class="input-row">
819
+ <textarea class="input-field" id="user-input" placeholder="输入您的问题..." rows="2"></textarea>
820
+ <button class="send-button" id="send-button">
821
+ <i class="bi bi-send"></i>
822
+ </button>
823
+ </div>
824
+ </div>
825
+ </div>
826
+
827
+ <!-- 代码执行插件 -->
828
+ <div class="plugin-container code-plugin" id="code-plugin">
829
+ <div class="plugin-header">
830
+ <h3 class="plugin-title"><i class="bi bi-code-square"></i> Python代码执行</h3>
831
+ <button class="plugin-close" id="close-code-plugin">
832
+ <i class="bi bi-x-lg"></i>
833
+ </button>
834
+ </div>
835
+ <div class="plugin-content">
836
+ <iframe id="code-execution-frame" src="" style="width: 100%; height: 100%; border: none;"></iframe>
837
+ </div>
838
+ </div>
839
+
840
+ <!-- 3D可视化插件 -->
841
+ <div class="plugin-container visualization-plugin" id="visualization-plugin">
842
+ <div class="plugin-header">
843
+ <h3 class="plugin-title"><i class="bi bi-graph-up"></i> 3D可视化</h3>
844
+ <button class="plugin-close" id="close-visualization-plugin">
845
+ <i class="bi bi-x-lg"></i>
846
+ </button>
847
+ </div>
848
+ <div class="plugin-content">
849
+ <div class="visualization-result" id="visualization-result">
850
+ <div class="text-center py-4">
851
+ <div class="spinner-border text-primary" role="status">
852
+ <span class="visually-hidden">加载中...</span>
853
+ </div>
854
+ <p class="mt-3">正在准备3D可视化...</p>
855
+ </div>
856
+ </div>
857
+ </div>
858
+ </div>
859
+
860
+ <!-- 思维导图插件 -->
861
+ <div class="plugin-container mindmap-plugin" id="mindmap-plugin">
862
+ <div class="plugin-header">
863
+ <h3 class="plugin-title"><i class="bi bi-diagram-3"></i> 思维导图</h3>
864
+ <button class="plugin-close" id="close-mindmap-plugin">
865
+ <i class="bi bi-x-lg"></i>
866
+ </button>
867
+ </div>
868
+ <div class="plugin-content">
869
+ <div class="mindmap-result" id="mindmap-result">
870
+ <div class="text-center py-4">
871
+ <div class="spinner-border text-primary" role="status">
872
+ <span class="visually-hidden">加载中...</span>
873
+ </div>
874
+ <p class="mt-3">正在生成思维导图...</p>
875
+ </div>
876
+ </div>
877
+ </div>
878
+ </div>
879
+ </div>
880
+
881
+ <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/katex.min.js"></script>
882
+ <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/contrib/auto-render.min.js"></script>
883
+ <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/markdown-it.min.js"></script>
884
+ <script>
885
+ // 全局变量
886
+ const agentId = "{{ agent_id }}";
887
+ const token = "{{ token }}";
888
+ let executionContext = null;
889
+ const mainContainer = document.getElementById('main-container');
890
+ let isTyping = false;
891
+ let typewriterInterval = null;
892
+ let typewriterPaused = false;
893
+ let currentMessageQueue = [];
894
+ let typewriterSpeed = 20; // 打字速度(ms)
895
+ let currentMessageElement = null;
896
+
897
+ // DOM元素
898
+ const chatMessages = document.getElementById('chat-messages');
899
+ const userInput = document.getElementById('user-input');
900
+ const sendButton = document.getElementById('send-button');
901
+
902
+ // 代码执行插件元素
903
+ const codePlugin = document.getElementById('code-plugin');
904
+ const closeCodePlugin = document.getElementById('close-code-plugin');
905
+ const codeExecutionFrame = document.getElementById('code-execution-frame');
906
+
907
+ // 3D可视化插件元素
908
+ const visualizationPlugin = document.getElementById('visualization-plugin');
909
+ const closeVisualizationPlugin = document.getElementById('close-visualization-plugin');
910
+ const visualizationResult = document.getElementById('visualization-result');
911
+
912
+ // 思维导图插件元素
913
+ const mindmapPlugin = document.getElementById('mindmap-plugin');
914
+ const closeMindmapPlugin = document.getElementById('close-mindmap-plugin');
915
+ const mindmapResult = document.getElementById('mindmap-result');
916
+
917
+ // 输入建议
918
+ const inputSuggestions = document.getElementById('input-suggestions');
919
+
920
+ // Markdown-it 实例
921
+ const md = window.markdownit({
922
+ html: false,
923
+ linkify: true,
924
+ typographer: true,
925
+ breaks: true,
926
+ highlight: function (str, lang) {
927
+ if (lang && hljs.getLanguage(lang)) {
928
+ try {
929
+ const highlighted = hljs.highlight(str, { language: lang }).value;
930
+ return `<pre><code class="hljs language-${lang}">${highlighted}</code><button class="copy-button" onclick="copyToClipboard(this)"><i class="bi bi-clipboard"></i> 复制</button></pre>`;
931
+ } catch (__) {}
932
+ }
933
+ return `<pre><code class="hljs">${md.utils.escapeHtml(str)}</code><button class="copy-button" onclick="copyToClipboard(this)"><i class="bi bi-clipboard"></i> 复制</button></pre>`;
934
+ }
935
+ });
936
+
937
+ // 页面加载完成后执行
938
+ document.addEventListener('DOMContentLoaded', function() {
939
+ // 初始化发送按钮事件
940
+ sendButton.addEventListener('click', sendMessage);
941
+
942
+ // 初始化输入框回车事件
943
+ userInput.addEventListener('keydown', function(e) {
944
+ if (e.key === 'Enter' && !e.shiftKey) {
945
+ e.preventDefault();
946
+ sendMessage();
947
+ }
948
+ });
949
+
950
+ // 初始化问题建议按钮
951
+ document.querySelectorAll('.suggestion-button').forEach(button => {
952
+ button.addEventListener('click', function() {
953
+ userInput.value = this.textContent;
954
+ sendMessage();
955
+ });
956
+ });
957
+
958
+ // 初始化插件关闭事件
959
+ closeCodePlugin.addEventListener('click', () => {
960
+ codePlugin.style.display = 'none';
961
+ // 清空iframe源以停止任何运行中的代码
962
+ codeExecutionFrame.src = '';
963
+ updateMainContainerLayout();
964
+ });
965
+
966
+ closeVisualizationPlugin.addEventListener('click', () => {
967
+ visualizationPlugin.style.display = 'none';
968
+ updateMainContainerLayout();
969
+ });
970
+
971
+ closeMindmapPlugin.addEventListener('click', () => {
972
+ mindmapPlugin.style.display = 'none';
973
+ updateMainContainerLayout();
974
+ });
975
+
976
+ // 焦点事件
977
+ userInput.addEventListener('focus', function() {
978
+ // inputSuggestions.style.display = 'block';
979
+ });
980
+
981
+ userInput.addEventListener('blur', function(e) {
982
+ // 延迟执行,以便点击建议按钮事件先触发
983
+ setTimeout(() => {
984
+ // inputSuggestions.style.display = 'none';
985
+ }, 100);
986
+ });
987
+ });
988
+
989
+ // 更新主容器布局
990
+ function updateMainContainerLayout() {
991
+ // 检查是否有任何插件处于显示状态
992
+ const isAnyPluginVisible =
993
+ codePlugin.style.display === 'flex' ||
994
+ visualizationPlugin.style.display === 'flex' ||
995
+ mindmapPlugin.style.display === 'flex';
996
+
997
+ // 更新主容器类名
998
+ if (isAnyPluginVisible) {
999
+ mainContainer.classList.add('with-plugin');
1000
+ } else {
1001
+ mainContainer.classList.remove('with-plugin');
1002
+ }
1003
+ }
1004
+
1005
+ // 发送消息
1006
+ async function sendMessage() {
1007
+ const message = userInput.value.trim();
1008
+ if (!message || isTyping) return;
1009
+
1010
+ // 添加用户消息
1011
+ addMessage(message, true);
1012
+
1013
+ // 清空输入框
1014
+ userInput.value = '';
1015
+
1016
+ // 禁用发送按钮
1017
+ sendButton.disabled = true;
1018
+ isTyping = true;
1019
+
1020
+ // 添加机器人正在输入的指示
1021
+ const typingIndicator = addTypingIndicator();
1022
+
1023
+ try {
1024
+ // 发送请求
1025
+ const response = await fetch(`/api/student/chat/${agentId}`, {
1026
+ method: 'POST',
1027
+ headers: {
1028
+ 'Content-Type': 'application/json'
1029
+ },
1030
+ body: JSON.stringify({
1031
+ message: message,
1032
+ token: token
1033
+ })
1034
+ });
1035
+
1036
+ const data = await response.json();
1037
+
1038
+ // 移除输入指示器
1039
+ if (typingIndicator) {
1040
+ typingIndicator.remove();
1041
+ }
1042
+
1043
+ if (data.success) {
1044
+ // 处理回复内容,移除参考来源部分
1045
+ const processedContent = processResponseContent(data.message);
1046
+
1047
+ // 添加机器人回复(带打字机效果)
1048
+ const messageElement = addMessage(processedContent, false);
1049
+
1050
+ // 添加参考信息(如果有)
1051
+ if (data.references && data.references.length > 0) {
1052
+ // 等待打字机效果完成后添加参考信息
1053
+ const checkTypewriterComplete = setInterval(() => {
1054
+ if (!typewriterInterval) {
1055
+ clearInterval(checkTypewriterComplete);
1056
+ addReferences(messageElement, data.references);
1057
+ }
1058
+ }, 100);
1059
+ }
1060
+
1061
+ // 检查是否需要显示工具
1062
+ if (data.tools && data.tools.length > 0) {
1063
+ // 隐藏所有插件
1064
+ hideAllPlugins();
1065
+
1066
+ // 使用新的插件激活函数
1067
+ activatePlugins(data.message, data.tools);
1068
+ }
1069
+ } else {
1070
+ // 添加错误消息
1071
+ addMessage(`错误: ${data.message}`, false);
1072
+ }
1073
+ } catch (error) {
1074
+ console.error('发送请求出错:', error);
1075
+ // 移除输入指示器
1076
+ if (typingIndicator) {
1077
+ typingIndicator.remove();
1078
+ }
1079
+ addMessage('发送请求时出错,请重试', false);
1080
+ } finally {
1081
+ // 恢复发送按钮
1082
+ sendButton.disabled = false;
1083
+ isTyping = false;
1084
+ }
1085
+ }
1086
+
1087
+ // 添加打字指示器
1088
+ function addTypingIndicator() {
1089
+ const botMessage = document.createElement('div');
1090
+ botMessage.className = 'message bot-message';
1091
+
1092
+ const content = document.createElement('div');
1093
+ content.className = 'message-content';
1094
+ content.innerHTML = `
1095
+ <p>
1096
+ <span class="typing-indicator">
1097
+ <span class="typing-dot"></span>
1098
+ <span class="typing-dot"></span>
1099
+ <span class="typing-dot"></span>
1100
+ </span>
1101
+ </p>
1102
+ `;
1103
+
1104
+ botMessage.appendChild(content);
1105
+ chatMessages.appendChild(botMessage);
1106
+
1107
+ // 滚动到底部
1108
+ chatMessages.scrollTop = chatMessages.scrollHeight;
1109
+
1110
+ return botMessage;
1111
+ }
1112
+
1113
+ // 处理回复内容,移除参考来源部分
1114
+ function processResponseContent(content) {
1115
+ // 移除 "===参考来源开始===" 到 "===参考来源结束===" 之间的内容
1116
+ return content.replace(/===参考来源开始===[\s\S]*?===参考来源结束===/, '');
1117
+ }
1118
+
1119
+ // 添加参考信息显示函数
1120
+ function addReferences(messageElement, references) {
1121
+ // 创建参考容器
1122
+ const referenceContainer = document.createElement('div');
1123
+ referenceContainer.className = 'reference-container';
1124
+
1125
+ // 创建参考切换按钮
1126
+ const toggleButton = document.createElement('div');
1127
+ toggleButton.className = 'reference-toggle';
1128
+ toggleButton.innerHTML = '<i class="bi bi-journal-text"></i> 参考来源';
1129
+
1130
+ // 创建参考内容
1131
+ const referenceContent = document.createElement('div');
1132
+ referenceContent.className = 'reference-content';
1133
+
1134
+ // 添加参考项
1135
+ references.forEach(ref => {
1136
+ const refItem = document.createElement('div');
1137
+ refItem.className = 'reference-item';
1138
+
1139
+ refItem.innerHTML = `
1140
+ <div><strong>[${ref.index}]</strong> ${ref.summary}</div>
1141
+ <div class="reference-item-source">来源: ${ref.file_name}</div>
1142
+ `;
1143
+
1144
+ referenceContent.appendChild(refItem);
1145
+ });
1146
+
1147
+ // 添加切换功能
1148
+ toggleButton.addEventListener('click', () => {
1149
+ if (referenceContent.style.display === 'block') {
1150
+ referenceContent.style.display = 'none';
1151
+ toggleButton.innerHTML = '<i class="bi bi-journal-text"></i> 参考来源';
1152
+ } else {
1153
+ referenceContent.style.display = 'block';
1154
+ toggleButton.innerHTML = '<i class="bi bi-journal-arrow-up"></i> 收起参考来源';
1155
+ }
1156
+ });
1157
+
1158
+ // 组装并添加到消息
1159
+ referenceContainer.appendChild(toggleButton);
1160
+ referenceContainer.appendChild(referenceContent);
1161
+ messageElement.appendChild(referenceContainer);
1162
+ }
1163
+
1164
+ // 添加消息函数
1165
+ function addMessage(content, isUser) {
1166
+ const messageDiv = document.createElement('div');
1167
+ messageDiv.className = isUser ? 'message user-message' : 'message bot-message';
1168
+
1169
+ const messageContent = document.createElement('div');
1170
+ messageContent.className = 'message-content';
1171
+
1172
+ if (isUser) {
1173
+ // 直接显示用户消息,仅替换换行符
1174
+ messageContent.innerHTML = `<p>${content.replace(/\n/g, '<br>')}</p>`;
1175
+ } else {
1176
+ // 为AI消息添加打字机效果
1177
+ messageContent.innerHTML = '<p></p>';
1178
+ // 添加控制按钮
1179
+ const controlsDiv = document.createElement('div');
1180
+ controlsDiv.className = 'typewriter-controls';
1181
+ controlsDiv.innerHTML = `
1182
+ <button class="typewriter-btn pause-btn" title="暂停"><i class="bi bi-pause-fill"></i></button>
1183
+ <button class="typewriter-btn continue-btn" style="display:none;" title="继续"><i class="bi bi-play-fill"></i></button>
1184
+ <button class="typewriter-btn speed-btn" title="加速"><i class="bi bi-lightning-fill"></i></button>
1185
+ `;
1186
+ messageContent.appendChild(controlsDiv);
1187
+
1188
+ // 添加事件监听器
1189
+ const pauseBtn = controlsDiv.querySelector('.pause-btn');
1190
+ const continueBtn = controlsDiv.querySelector('.continue-btn');
1191
+ const speedBtn = controlsDiv.querySelector('.speed-btn');
1192
+
1193
+ pauseBtn.addEventListener('click', () => {
1194
+ pauseTypewriter();
1195
+ pauseBtn.style.display = 'none';
1196
+ continueBtn.style.display = 'inline-block';
1197
+ });
1198
+
1199
+ continueBtn.addEventListener('click', () => {
1200
+ continueTypewriter();
1201
+ continueBtn.style.display = 'none';
1202
+ pauseBtn.style.display = 'inline-block';
1203
+ });
1204
+
1205
+ speedBtn.addEventListener('click', () => {
1206
+ toggleTypewriterSpeed();
1207
+ });
1208
+
1209
+ // 开始打字机效果
1210
+ startTypewriter(content, messageContent.querySelector('p'));
1211
+ }
1212
+
1213
+ messageDiv.appendChild(messageContent);
1214
+ chatMessages.appendChild(messageDiv);
1215
+
1216
+ // 滚动到底部
1217
+ chatMessages.scrollTop = chatMessages.scrollHeight;
1218
+
1219
+ return messageDiv;
1220
+ }
1221
+
1222
+ // 打字机效果函数
1223
+ function startTypewriter(text, element) {
1224
+ // 清除之前的打字机效果(如果有)
1225
+ if (typewriterInterval) {
1226
+ clearInterval(typewriterInterval);
1227
+ }
1228
+
1229
+ // 保存当前消息元素引用
1230
+ currentMessageElement = element;
1231
+
1232
+ // 初始化
1233
+ typewriterPaused = false;
1234
+ let mdContent = md.render(text);
1235
+
1236
+ // 将HTML内容转换为文本节点和元素节点的队列
1237
+ parseHTMLToQueue(mdContent);
1238
+
1239
+ // 开始逐字显示
1240
+ typewriterInterval = setInterval(typeNextChar, typewriterSpeed);
1241
+ }
1242
+
1243
+ // 解析HTML到队列
1244
+ function parseHTMLToQueue(html) {
1245
+ // 清空队列
1246
+ currentMessageQueue = [];
1247
+
1248
+ // 创建临时容器
1249
+ const tempDiv = document.createElement('div');
1250
+ tempDiv.innerHTML = html;
1251
+
1252
+ // 递归处理所有子节点
1253
+ processNode(tempDiv);
1254
+ }
1255
+
1256
+ // 递归处理节点
1257
+ function processNode(node) {
1258
+ // 遍历节点的所有子节点
1259
+ for (let i = 0; i < node.childNodes.length; i++) {
1260
+ const child = node.childNodes[i];
1261
+
1262
+ if (child.nodeType === Node.TEXT_NODE) {
1263
+ // 文本节点,将每个字符加入队列
1264
+ for (let j = 0; j < child.textContent.length; j++) {
1265
+ currentMessageQueue.push({
1266
+ type: 'text',
1267
+ content: child.textContent[j]
1268
+ });
1269
+ }
1270
+ } else if (child.nodeType === Node.ELEMENT_NODE) {
1271
+ // 元素节点,将开始标签加入队列
1272
+ currentMessageQueue.push({
1273
+ type: 'element-start',
1274
+ tagName: child.tagName.toLowerCase(),
1275
+ attributes: Array.from(child.attributes)
1276
+ });
1277
+
1278
+ // 递归处理子节点
1279
+ processNode(child);
1280
+
1281
+ // 将结束标签加入队列
1282
+ currentMessageQueue.push({
1283
+ type: 'element-end',
1284
+ tagName: child.tagName.toLowerCase()
1285
+ });
1286
+ }
1287
+ }
1288
+ }
1289
+
1290
+ // 显示下一个字符
1291
+ function typeNextChar() {
1292
+ if (typewriterPaused || currentMessageQueue.length === 0 || !currentMessageElement) {
1293
+ return;
1294
+ }
1295
+
1296
+ let item = currentMessageQueue.shift();
1297
+
1298
+ if (item.type === 'text') {
1299
+ // 处理文本
1300
+ currentMessageElement.innerHTML += item.content;
1301
+ } else if (item.type === 'element-start') {
1302
+ // 处理元素开始标签
1303
+ const el = document.createElement(item.tagName);
1304
+ if (item.attributes) {
1305
+ item.attributes.forEach(attr => {
1306
+ el.setAttribute(attr.name, attr.value);
1307
+ });
1308
+ }
1309
+ currentMessageElement.appendChild(el);
1310
+ // 更新当前元素引用
1311
+ currentMessageElement = el;
1312
+ } else if (item.type === 'element-end') {
1313
+ // 处理元素结束标签
1314
+ // 将当前元素引用更新为父元素
1315
+ if (currentMessageElement.parentElement) {
1316
+ currentMessageElement = currentMessageElement.parentElement;
1317
+ }
1318
+ }
1319
+
1320
+ // 滚动到底部
1321
+ chatMessages.scrollTop = chatMessages.scrollHeight;
1322
+
1323
+ // 如果队列为空,需要渲染数学公式
1324
+ if (currentMessageQueue.length === 0) {
1325
+ clearInterval(typewriterInterval);
1326
+ typewriterInterval = null;
1327
+
1328
+ // 找到最近添加的机器人消息
1329
+ const botMessage = chatMessages.querySelector('.bot-message:last-child');
1330
+ if (botMessage) {
1331
+ try {
1332
+ renderMathInElement(botMessage.querySelector('.message-content'), {
1333
+ delimiters: [
1334
+ {left: '$$', right: '$$', display: true},
1335
+ {left: '$', right: '$', display: false},
1336
+ {left: '\\(', right: '\\)', display: false},
1337
+ {left: '\\[', right: '\\]', display: true}
1338
+ ],
1339
+ throwOnError: false
1340
+ });
1341
+ } catch (e) {
1342
+ console.error('渲染LaTeX出错:', e);
1343
+ }
1344
+
1345
+ // 为代码块添加复制按钮
1346
+ botMessage.querySelectorAll('pre').forEach(pre => {
1347
+ if (!pre.querySelector('.copy-button')) {
1348
+ const copyButton = document.createElement('button');
1349
+ copyButton.className = 'copy-button';
1350
+ copyButton.innerHTML = '<i class="bi bi-clipboard"></i> 复制';
1351
+ copyButton.onclick = function() { copyToClipboard(this); };
1352
+ pre.appendChild(copyButton);
1353
+ }
1354
+ });
1355
+ }
1356
+ }
1357
+ }
1358
+
1359
+ // 暂停打字机效果
1360
+ function pauseTypewriter() {
1361
+ typewriterPaused = true;
1362
+ }
1363
+
1364
+ // 继续打字机效果
1365
+ function continueTypewriter() {
1366
+ typewriterPaused = false;
1367
+
1368
+ // 如果之前的定时器已清除,重新启动
1369
+ if (!typewriterInterval && currentMessageQueue.length > 0) {
1370
+ typewriterInterval = setInterval(typeNextChar, typewriterSpeed);
1371
+ }
1372
+ }
1373
+
1374
+ // 切换打字速度
1375
+ function toggleTypewriterSpeed() {
1376
+ if (typewriterSpeed === 20) {
1377
+ typewriterSpeed = 5; // 快速
1378
+ } else {
1379
+ typewriterSpeed = 20; // 正常速度
1380
+ }
1381
+
1382
+ // 如果正在运行,更新速度
1383
+ if (typewriterInterval) {
1384
+ clearInterval(typewriterInterval);
1385
+ typewriterInterval = setInterval(typeNextChar, typewriterSpeed);
1386
+ }
1387
+ }
1388
+
1389
+ // 复制代码到剪贴板
1390
+ function copyToClipboard(button) {
1391
+ const codeBlock = button.parentNode.querySelector('code');
1392
+ const text = codeBlock.textContent;
1393
+
1394
+ navigator.clipboard.writeText(text).then(() => {
1395
+ button.innerHTML = '<i class="bi bi-check"></i> 已复制';
1396
+
1397
+ setTimeout(() => {
1398
+ button.innerHTML = '<i class="bi bi-clipboard"></i> 复制';
1399
+ }, 2000);
1400
+ }).catch(err => {
1401
+ console.error('复制失败:', err);
1402
+ button.innerHTML = '<i class="bi bi-exclamation-triangle"></i> 失败';
1403
+
1404
+ setTimeout(() => {
1405
+ button.innerHTML = '<i class="bi bi-clipboard"></i> 复制';
1406
+ }, 2000);
1407
+ });
1408
+ }
1409
+
1410
+ // 隐藏所有插件
1411
+ function hideAllPlugins() {
1412
+ codePlugin.style.display = 'none';
1413
+ visualizationPlugin.style.display = 'none';
1414
+ mindmapPlugin.style.display = 'none';
1415
+ updateMainContainerLayout();
1416
+ }
1417
+
1418
+ // 提取代码块
1419
+ function extractCodeBlocks(message) {
1420
+ const codeBlocks = [];
1421
+ const codeRegex = /```python\n([\s\S]*?)\n```/g;
1422
+
1423
+ let match;
1424
+ while ((match = codeRegex.exec(message)) !== null) {
1425
+ codeBlocks.push(match[1]);
1426
+ }
1427
+
1428
+ return codeBlocks;
1429
+ }
1430
+
1431
+ // 提取3D可视化函数代码
1432
+ function extract3DVisualizationCode(message) {
1433
+ // 尝试提取函数代码块
1434
+ const codeRegex = /```python\s*(import[\s\S]*?def create_3d_plot\(\):[\s\S]*?return[\s\S]*?})\s*```/i;
1435
+ const match = codeRegex.exec(message);
1436
+
1437
+ if (match) {
1438
+ return match[1].trim();
1439
+ }
1440
+
1441
+ return null;
1442
+ }
1443
+
1444
+ // 提取思维导图内容
1445
+ function extractMindmapContent(message) {
1446
+ // 尝试提取@startmindmap和@endmindmap之间的内容
1447
+ const mindmapRegex = /@startmindmap\n([\s\S]*?)@endmindmap/;
1448
+ const match = mindmapRegex.exec(message);
1449
+
1450
+ if (match) {
1451
+ return `@startmindmap\n${match[1]}\n@endmindmap`;
1452
+ }
1453
+
1454
+ return null;
1455
+ }
1456
+
1457
+ // 激活代码执行插件
1458
+ function activateCodePlugin(message) {
1459
+ // 提取代码块
1460
+ const codeBlocks = extractCodeBlocks(message);
1461
+
1462
+ if (codeBlocks.length > 0) {
1463
+ const isAlreadyVisible = codePlugin.style.display === 'flex';
1464
+
1465
+ // 显示插件容器
1466
+ codePlugin.style.display = 'flex';
1467
+ updateMainContainerLayout();
1468
+
1469
+ // 获取iframe
1470
+ const iframe = document.getElementById('code-execution-frame');
1471
+
1472
+ // 如果插件已经可见且iframe已加载
1473
+ if (isAlreadyVisible && iframe.contentWindow) {
1474
+ // 发送新代码到现有iframe
1475
+ iframe.contentWindow.postMessage({
1476
+ type: 'setCode',
1477
+ code: codeBlocks[0]
1478
+ }, '*');
1479
+ } else {
1480
+ // 设置iframe源
1481
+ let src = '/code_execution.html';
1482
+ if (codeBlocks.length > 0) {
1483
+ src += `?code=${encodeURIComponent(codeBlocks[0])}`;
1484
+ }
1485
+
1486
+ iframe.src = src;
1487
+
1488
+ // 设置iframe加载事件处理程序
1489
+ iframe.onload = function() {
1490
+ if (codeBlocks.length > 0) {
1491
+ iframe.contentWindow.postMessage({
1492
+ type: 'setCode',
1493
+ code: codeBlocks[0]
1494
+ }, '*');
1495
+ }
1496
+ };
1497
+ }
1498
+ }
1499
+ }
1500
+
1501
+ // 激活3D可视化插件
1502
+ function activate3DVisualization(message) {
1503
+ const code = extract3DVisualizationCode(message);
1504
+
1505
+ if (code) {
1506
+ // 显示插件容器
1507
+ visualizationPlugin.style.display = 'flex';
1508
+ updateMainContainerLayout();
1509
+
1510
+ // 显示加载状态
1511
+ visualizationResult.innerHTML = `
1512
+ <div class="text-center py-4">
1513
+ <div class="spinner-border" style="color: var(--secondary-color);" role="status">
1514
+ <span class="visually-hidden">生成中...</span>
1515
+ </div>
1516
+ <p class="mt-3">正在生成3D图形,请稍候...</p>
1517
+ </div>
1518
+ `;
1519
+
1520
+ // 自动生成可视化
1521
+ fetch('/api/visualization/3d-surface', {
1522
+ method: 'POST',
1523
+ headers: {
1524
+ 'Content-Type': 'application/json'
1525
+ },
1526
+ body: JSON.stringify({ code: code })
1527
+ })
1528
+ .then(response => response.json())
1529
+ .then(data => {
1530
+ if (data.success) {
1531
+ // 直接嵌入HTML
1532
+ visualizationResult.innerHTML = `
1533
+ <div class="visualization-iframe-container" style="width:100%; height:500px;">
1534
+ <iframe src="${data.html_url}" style="width:100%; height:100%; border:none;"></iframe>
1535
+ </div>
1536
+ <div class="text-center mt-3">
1537
+ <p>3D图形生成成功</p>
1538
+ <a href="${data.html_url}" class="btn btn-sm btn-outline-primary" target="_blank">
1539
+ <i class="bi bi-arrows-fullscreen"></i> 全屏查看
1540
+ </a>
1541
+ </div>
1542
+ `;
1543
+ } else {
1544
+ visualizationResult.innerHTML = `
1545
+ <div class="alert alert-danger">
1546
+ <i class="bi bi-exclamation-triangle-fill me-2"></i>
1547
+ 生成失败: ${data.message}
1548
+ </div>
1549
+ `;
1550
+ }
1551
+ })
1552
+ .catch(error => {
1553
+ console.error('生成3D图形出错:', error);
1554
+ visualizationResult.innerHTML = `
1555
+ <div class="alert alert-danger">
1556
+ <i class="bi bi-exclamation-triangle-fill me-2"></i>
1557
+ 生成3D图形时发生错误,请重试
1558
+ </div>
1559
+ `;
1560
+ });
1561
+ }
1562
+ }
1563
+
1564
+ // 激活思维导图插件
1565
+ function activateMindmap(message) {
1566
+ const content = extractMindmapContent(message);
1567
+
1568
+ if (content) {
1569
+ // 显示插件容器
1570
+ mindmapPlugin.style.display = 'flex';
1571
+ updateMainContainerLayout();
1572
+
1573
+ // 显示加载状态
1574
+ mindmapResult.innerHTML = `
1575
+ <div class="text-center py-4">
1576
+ <div class="spinner-border" style="color: var(--secondary-color);" role="status">
1577
+ <span class="visually-hidden">生成中...</span>
1578
+ </div>
1579
+ <p class="mt-3">正在生成思维导图,请稍候...</p>
1580
+ </div>
1581
+ `;
1582
+
1583
+ // 自动生成思维导图
1584
+ fetch('/api/visualization/mindmap', {
1585
+ method: 'POST',
1586
+ headers: {
1587
+ 'Content-Type': 'application/json'
1588
+ },
1589
+ body: JSON.stringify({ content: content })
1590
+ })
1591
+ .then(response => response.json())
1592
+ .then(data => {
1593
+ if (data.success) {
1594
+ mindmapResult.innerHTML = `
1595
+ <div class="text-center">
1596
+ <img src="${data.url}" class="mindmap-image" alt="思维导图">
1597
+ <p class="mt-3">生成成功!</p>
1598
+ <a href="${data.url}" class="btn btn-sm btn-outline-primary mt-2" target="_blank">
1599
+ <i class="bi bi-download"></i> 下载图片
1600
+ </a>
1601
+ </div>
1602
+ `;
1603
+ } else {
1604
+ mindmapResult.innerHTML = `
1605
+ <div class="alert alert-danger">
1606
+ <i class="bi bi-exclamation-triangle-fill me-2"></i>
1607
+ 生成失败: ${data.message}
1608
+ </div>
1609
+ `;
1610
+ }
1611
+ })
1612
+ .catch(error => {
1613
+ console.error('生成思维导图出错:', error);
1614
+ mindmapResult.innerHTML = `
1615
+ <div class="alert alert-danger">
1616
+ <i class="bi bi-exclamation-triangle-fill me-2"></i>
1617
+ 生成思维导图时发生错误,请重试
1618
+ </div>
1619
+ `;
1620
+ });
1621
+ }
1622
+ }
1623
+
1624
+ // 根据消息内容激活适当的插件
1625
+ function activatePlugins(message, tools) {
1626
+ // 检查并激活代码执行插件
1627
+ if (tools.includes('code') && message.includes('```python')) {
1628
+ activateCodePlugin(message);
1629
+ }
1630
+
1631
+ // 检查并激活3D可视化插件
1632
+ if (tools.includes('visualization') &&
1633
+ (message.includes('def create_3d_plot') ||
1634
+ message.includes('3D') || message.includes('可视化'))) {
1635
+ activate3DVisualization(message);
1636
+ }
1637
+
1638
+ // 检查并激活思维导图插件
1639
+ if (tools.includes('mindmap') &&
1640
+ (message.includes('@startmindmap') ||
1641
+ message.includes('思维导图'))) {
1642
+ activateMindmap(message);
1643
+ }
1644
+ }
1645
+ </script>
1646
+
1647
+ <!-- 添加highlight.js用于代码高亮 -->
1648
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.8.0/highlight.min.js"></script>
1649
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.8.0/languages/python.min.js"></script>
1650
+ </body>
1651
+ </html>