ChaseHan commited on
Commit
4174457
·
verified ·
1 Parent(s): 96964f6

Create app.py

Browse files
Files changed (1) hide show
  1. app.py +1702 -0
app.py ADDED
@@ -0,0 +1,1702 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Educational LLM Application Based on Gradio
3
+ """
4
+
5
+ import os
6
+ import gradio as gr
7
+ from typing import Dict, Any, List, Tuple, Optional
8
+ from visualization import create_network_graph
9
+ from llm_utils import decompose_concepts, get_concept_explanation, call_llm
10
+ from concept_handler import MOCK_DECOMPOSITION_RESULT, MOCK_EXPLANATION_RESULT
11
+ from config import DEBUG_MODE
12
+ from fastapi import FastAPI
13
+ from fastapi.responses import JSONResponse
14
+ import json
15
+
16
+ # Custom CSS styles
17
+ custom_css = """
18
+ /* Global styles */
19
+ body {
20
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Microsoft YaHei", "WenQuanYi Micro Hei", sans-serif;
21
+ color: #333;
22
+ background-color: #f7f9fc;
23
+ }
24
+
25
+ /* Heading styles */
26
+ h1 {
27
+ color: #2c3e50;
28
+ font-weight: 700;
29
+ margin-bottom: 1rem;
30
+ }
31
+
32
+ h2, h3, h4 {
33
+ color: #3498db;
34
+ font-weight: 600;
35
+ }
36
+
37
+ /* Button styles */
38
+ button.primary {
39
+ background: linear-gradient(135deg, #3498db, #2980b9);
40
+ border: none;
41
+ box-shadow: 0 4px 6px rgba(52, 152, 219, 0.3);
42
+ }
43
+
44
+ button.secondary {
45
+ background: #ecf0f1;
46
+ color: #2980b9;
47
+ border: 1px solid #bdc3c7;
48
+ }
49
+
50
+ button:hover {
51
+ transform: translateY(-2px);
52
+ box-shadow: 0 6px 8px rgba(0, 0, 0, 0.1);
53
+ transition: all 0.3s ease;
54
+ }
55
+
56
+ /* Input fields enhancement */
57
+ input, textarea, select {
58
+ border: 1px solid #ddd;
59
+ border-radius: 8px;
60
+ padding: 10px;
61
+ box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.05);
62
+ transition: all 0.3s ease;
63
+ }
64
+
65
+ input:focus, textarea:focus, select:focus {
66
+ border-color: #3498db;
67
+ box-shadow: 0 0 0 2px rgba(52, 152, 219, 0.25);
68
+ }
69
+
70
+ /* Tab styles */
71
+ .tabs {
72
+ border-bottom: 2px solid #e0e0e0;
73
+ }
74
+
75
+ .tab-selected {
76
+ color: #3498db;
77
+ border-bottom: 2px solid #3498db;
78
+ }
79
+
80
+ /* Concept card styles */
81
+ .concept-card {
82
+ transition: all 0.3s ease;
83
+ border: 1px solid #e0e0e0;
84
+ border-radius: 12px;
85
+ padding: 16px;
86
+ margin-bottom: 16px;
87
+ cursor: pointer;
88
+ background-color: #fff;
89
+ box-shadow: 0 2px 5px rgba(0,0,0,0.05);
90
+ }
91
+
92
+ .concept-card:hover {
93
+ box-shadow: 0 5px 15px rgba(0,0,0,0.1);
94
+ transform: translateY(-3px);
95
+ border-color: #bdc3c7;
96
+ }
97
+
98
+ .selected-card {
99
+ border-color: #3498db;
100
+ background-color: rgba(52, 152, 219, 0.05);
101
+ box-shadow: 0 0 0 2px rgba(52, 152, 219, 0.3);
102
+ }
103
+
104
+ .concept-title {
105
+ font-weight: bold;
106
+ margin-bottom: 8px;
107
+ color: #2c3e50;
108
+ font-size: 1.1em;
109
+ }
110
+
111
+ .concept-desc {
112
+ font-size: 0.95em;
113
+ color: #7f8c8d;
114
+ line-height: 1.5;
115
+ }
116
+
117
+ /* Knowledge graph styles */
118
+ #concept-graph {
119
+ background-color: #fff;
120
+ border-radius: 12px;
121
+ padding: 16px;
122
+ box-shadow: 0 2px 10px rgba(0,0,0,0.05);
123
+ }
124
+
125
+ #concept-graph img {
126
+ max-width: 100%;
127
+ height: auto;
128
+ border-radius: 8px;
129
+ box-shadow: 0 4px 15px rgba(0,0,0,0.1);
130
+ transition: all 0.3s ease;
131
+ }
132
+
133
+ #concept-graph img:hover {
134
+ box-shadow: 0 8px 25px rgba(0,0,0,0.15);
135
+ }
136
+
137
+ /* Example box styles */
138
+ .example-box {
139
+ background-color: #f1f8fe;
140
+ border-left: 4px solid #3498db;
141
+ padding: 15px;
142
+ margin: 15px 0;
143
+ border-radius: 0 8px 8px 0;
144
+ }
145
+
146
+ .example-box h4 {
147
+ margin-top: 0;
148
+ color: #2980b9;
149
+ }
150
+
151
+ /* Resource item styles */
152
+ .resource-item {
153
+ padding: 12px;
154
+ margin: 10px 0;
155
+ border-bottom: 1px dashed #e0e0e0;
156
+ transition: all 0.2s ease;
157
+ }
158
+
159
+ .resource-item:hover {
160
+ background-color: #f9f9f9;
161
+ }
162
+
163
+ /* Details and answers styles */
164
+ details {
165
+ margin: 10px 0;
166
+ padding: 10px;
167
+ border: 1px solid #e0e0e0;
168
+ border-radius: 8px;
169
+ background-color: #f9f9f9;
170
+ }
171
+
172
+ summary {
173
+ cursor: pointer;
174
+ color: #3498db;
175
+ font-weight: 600;
176
+ padding: 5px;
177
+ }
178
+
179
+ summary:hover {
180
+ color: #2980b9;
181
+ }
182
+
183
+ /* Layout container styles */
184
+ .container {
185
+ background-color: #fff;
186
+ border-radius: 12px;
187
+ padding: 20px;
188
+ box-shadow: 0 4px 6px rgba(0,0,0,0.05);
189
+ margin-bottom: 20px;
190
+ }
191
+
192
+ /* Responsive adjustments */
193
+ @media (max-width: 768px) {
194
+ .concept-card {
195
+ padding: 12px;
196
+ }
197
+
198
+ .example-box {
199
+ padding: 12px;
200
+ }
201
+ }
202
+
203
+ /* Answer box styles */
204
+ .answer-box {
205
+ background-color: #f8f9fa;
206
+ border-radius: 12px;
207
+ padding: 20px;
208
+ margin: 15px 0;
209
+ border: 1px solid #e9ecef;
210
+ box-shadow: 0 2px 4px rgba(0,0,0,0.05);
211
+ }
212
+
213
+ .answer-content {
214
+ margin-bottom: 15px;
215
+ line-height: 1.6;
216
+ color: #2c3e50;
217
+ }
218
+
219
+ .answer-content h4 {
220
+ color: #3498db;
221
+ margin-top: 0;
222
+ margin-bottom: 10px;
223
+ }
224
+
225
+ .main-concept {
226
+ background-color: #e3f2fd;
227
+ padding: 10px 15px;
228
+ border-radius: 8px;
229
+ color: #1976d2;
230
+ font-size: 0.95em;
231
+ }
232
+
233
+ .main-concept strong {
234
+ color: #1565c0;
235
+ }
236
+
237
+ /* Answer section styles */
238
+ .answer-section {
239
+ margin-top: 20px;
240
+ background: white;
241
+ border-radius: 15px;
242
+ padding: 20px;
243
+ }
244
+
245
+ .answer-box {
246
+ background-color: #f8f9fa;
247
+ border-radius: 12px;
248
+ padding: 20px;
249
+ margin: 15px 0;
250
+ border: 1px solid #e9ecef;
251
+ box-shadow: 0 2px 4px rgba(0,0,0,0.05);
252
+ transition: all 0.3s ease;
253
+ }
254
+
255
+ .answer-box:hover {
256
+ box-shadow: 0 4px 8px rgba(0,0,0,0.1);
257
+ }
258
+
259
+ .answer-content {
260
+ margin-bottom: 15px;
261
+ line-height: 1.6;
262
+ color: #2c3e50;
263
+ font-size: 1.1em;
264
+ }
265
+
266
+ .main-concept {
267
+ background-color: #e3f2fd;
268
+ padding: 12px 16px;
269
+ border-radius: 8px;
270
+ color: #1976d2;
271
+ font-size: 0.95em;
272
+ margin-top: 15px;
273
+ border: 1px solid rgba(25, 118, 210, 0.1);
274
+ }
275
+
276
+ .main-concept strong {
277
+ color: #1565c0;
278
+ font-weight: 600;
279
+ }
280
+
281
+ /* Loading animation styles */
282
+ .loading {
283
+ padding: 20px;
284
+ text-align: center;
285
+ color: #666;
286
+ font-size: 1.1em;
287
+ position: relative;
288
+ }
289
+
290
+ .loading:after {
291
+ content: '...';
292
+ position: absolute;
293
+ animation: dots 1.5s steps(5, end) infinite;
294
+ }
295
+
296
+ @keyframes dots {
297
+ 0%, 20% { content: '.'; }
298
+ 40% { content: '..'; }
299
+ 60% { content: '...'; }
300
+ 80%, 100% { content: ''; }
301
+ }
302
+
303
+ .loading::before {
304
+ content: '';
305
+ display: block;
306
+ width: 30px;
307
+ height: 30px;
308
+ border: 3px solid #3498db;
309
+ border-top-color: transparent;
310
+ border-radius: 50%;
311
+ margin: 0 auto 10px;
312
+ animation: spin 1s linear infinite;
313
+ }
314
+
315
+ @keyframes spin {
316
+ to { transform: rotate(360deg); }
317
+ }
318
+
319
+ /* 概念详解面板样式 */
320
+ .concept-explanation {
321
+ padding: 20px;
322
+ background: white;
323
+ border-radius: 12px;
324
+ box-shadow: 0 2px 8px rgba(0,0,0,0.1);
325
+ }
326
+
327
+ .explanation-content {
328
+ line-height: 1.6;
329
+ margin: 15px 0;
330
+ color: #2c3e50;
331
+ }
332
+
333
+ .examples-section {
334
+ margin-top: 20px;
335
+ }
336
+
337
+ .example-box {
338
+ background: #f8f9fa;
339
+ border-left: 4px solid #3498db;
340
+ padding: 15px;
341
+ margin: 15px 0;
342
+ border-radius: 0 8px 8px 0;
343
+ }
344
+
345
+ .example-problem {
346
+ margin-bottom: 10px;
347
+ color: #2c3e50;
348
+ }
349
+
350
+ .example-solution {
351
+ color: #34495e;
352
+ padding: 10px;
353
+ background: rgba(52, 152, 219, 0.05);
354
+ border-radius: 4px;
355
+ }
356
+
357
+ /* 加载动画样式 */
358
+ .loading {
359
+ text-align: center;
360
+ padding: 20px;
361
+ }
362
+
363
+ .loading-spinner {
364
+ border: 3px solid #f3f3f3;
365
+ border-top: 3px solid #3498db;
366
+ border-radius: 50%;
367
+ width: 30px;
368
+ height: 30px;
369
+ animation: spin 1s linear infinite;
370
+ margin: 0 auto 10px;
371
+ }
372
+
373
+ @keyframes spin {
374
+ 0% { transform: rotate(0deg); }
375
+ 100% { transform: rotate(360deg); }
376
+ }
377
+
378
+ .loading-text {
379
+ color: #666;
380
+ font-size: 0.9em;
381
+ }
382
+
383
+ /* 新增的样式 */
384
+ .concept-explanation-container {
385
+ margin-top: 20px;
386
+ padding: 15px;
387
+ background: white;
388
+ border-radius: 12px;
389
+ box-shadow: 0 2px 8px rgba(0,0,0,0.1);
390
+ transition: all 0.3s ease;
391
+ max-height: 600px; /* 固定最大高度 */
392
+ overflow-y: auto; /* 添加垂直滚动条 */
393
+
394
+ /* 自定义滚动条样式 */
395
+ scrollbar-width: thin;
396
+ scrollbar-color: #3498db #f1f1f1;
397
+ }
398
+
399
+ /* Webkit浏览器的滚动条样式 */
400
+ .concept-explanation-container::-webkit-scrollbar {
401
+ width: 8px;
402
+ }
403
+
404
+ .concept-explanation-container::-webkit-scrollbar-track {
405
+ background: #f1f1f1;
406
+ border-radius: 4px;
407
+ }
408
+
409
+ .concept-explanation-container::-webkit-scrollbar-thumb {
410
+ background: #3498db;
411
+ border-radius: 4px;
412
+ }
413
+
414
+ .concept-explanation-container::-webkit-scrollbar-thumb:hover {
415
+ background: #2980b9;
416
+ }
417
+
418
+ .concept-explanation-container h3 {
419
+ color: #2c3e50;
420
+ margin-bottom: 15px;
421
+ position: sticky;
422
+ top: 0;
423
+ background: white;
424
+ padding: 10px 0;
425
+ z-index: 1;
426
+ }
427
+
428
+ /* 内容区域样式 */
429
+ .card-explanation {
430
+ padding: 20px;
431
+ background: white;
432
+ border-radius: 12px;
433
+ }
434
+
435
+ .explanation-section, .key-points-section, .examples-section,
436
+ .practice-section, .resources-section {
437
+ margin-top: 20px;
438
+ padding: 15px;
439
+ background: #f8f9fa;
440
+ border-radius: 8px;
441
+ }
442
+
443
+ .explanation-section h4, .key-points-section h4,
444
+ .examples-section h4, .practice-section h4,
445
+ .resources-section h4 {
446
+ color: #2c3e50;
447
+ margin-bottom: 10px;
448
+ }
449
+
450
+ /* 修改generate_card_explanation函数中的标题文本 */
451
+ .explanation-section h4:before { content: "📚 Concept Explanation"; }
452
+ .key-points-section h4:before { content: "🎯 Key Points"; }
453
+ .examples-section h4:before { content: "📝 Example Analysis"; }
454
+ .practice-section h4:before { content: "✍️ Practice Problems"; }
455
+ .resources-section h4:before { content: "📚 Learning Resources"; }
456
+
457
+ /* 加载动画容器 */
458
+ .loading-container {
459
+ display: flex;
460
+ flex-direction: column;
461
+ align-items: center;
462
+ justify-content: center;
463
+ padding: 40px;
464
+ background: white;
465
+ border-radius: 12px;
466
+ box-shadow: 0 2px 8px rgba(0,0,0,0.1);
467
+ }
468
+
469
+ /* 加载动画旋转器 */
470
+ .loading-spinner {
471
+ width: 50px;
472
+ height: 50px;
473
+ border: 4px solid #f3f3f3;
474
+ border-top: 4px solid #3498db;
475
+ border-radius: 50%;
476
+ animation: spin 1s linear infinite;
477
+ margin-bottom: 20px;
478
+ }
479
+
480
+ /* 加载文本 */
481
+ .loading-text {
482
+ color: #666;
483
+ font-size: 1.1em;
484
+ margin-top: 10px;
485
+ }
486
+
487
+ /* 旋转动画 */
488
+ @keyframes spin {
489
+ 0% { transform: rotate(0deg); }
490
+ 100% { transform: rotate(360deg); }
491
+ }
492
+ """
493
+
494
+ # Custom JavaScript code
495
+ custom_js = """
496
+ // Handle concept card clicks
497
+ function conceptClick(conceptId) {
498
+ // Find the hidden input field and update its value
499
+ const conceptSelection = document.getElementById('concept-selection');
500
+ if (conceptSelection) {
501
+ conceptSelection.value = conceptId;
502
+ conceptSelection.dispatchEvent(new Event('input', { bubbles: true }));
503
+
504
+ // Highlight the selected card
505
+ document.querySelectorAll('.concept-card').forEach(card => {
506
+ card.classList.remove('selected-card');
507
+ if (card.getAttribute('data-concept-id') === conceptId) {
508
+ card.classList.add('selected-card');
509
+ }
510
+ });
511
+ }
512
+ }
513
+
514
+ // Enhance image display after loading
515
+ document.addEventListener('DOMContentLoaded', function() {
516
+ const graphContainer = document.getElementById('concept-graph');
517
+ if (graphContainer) {
518
+ const observer = new MutationObserver(function(mutations) {
519
+ mutations.forEach(function(mutation) {
520
+ if (mutation.addedNodes && mutation.addedNodes.length > 0) {
521
+ const img = graphContainer.querySelector('img');
522
+ if (img) {
523
+ img.style.maxWidth = '100%';
524
+ img.style.height = 'auto';
525
+ img.style.borderRadius = '8px';
526
+ img.style.boxShadow = '0 4px 8px rgba(0,0,0,0.1)';
527
+ }
528
+ }
529
+ });
530
+ });
531
+
532
+ observer.observe(graphContainer, { childList: true, subtree: true });
533
+ }
534
+ });
535
+ """
536
+
537
+ # Create cache directory
538
+ os.makedirs("cache", exist_ok=True)
539
+
540
+ # Global state storage
541
+ class AppState:
542
+ def __init__(self):
543
+ self.user_profile = {}
544
+ self.current_concepts_data = None
545
+ self.nodes_dict = {}
546
+ self.concepts_explanations = {} # Cache for generated concept explanations
547
+ self.concepts_expansions = {} # Cache for generated concept expansions
548
+ self.card_explanations = {} # 新增:缓存卡片点击生成的解释内容
549
+
550
+ def update_user_profile(self, grade: str, subject: str, needs: str) -> Dict[str, str]:
551
+ """Update user profile"""
552
+ self.user_profile = {
553
+ "grade": grade,
554
+ "subject": subject,
555
+ "needs": needs
556
+ }
557
+ return self.user_profile
558
+
559
+ def set_concepts_data(self, concepts_data: Dict[str, Any], nodes_dict: Dict[str, Any]):
560
+ """Set current concept data and node dictionary"""
561
+ self.current_concepts_data = concepts_data
562
+ self.nodes_dict = nodes_dict
563
+
564
+ def cache_concept_explanation(self, concept_id: str, explanation_data: Dict[str, Any]):
565
+ """Cache concept explanation data"""
566
+ self.concepts_explanations[concept_id] = explanation_data
567
+
568
+ def get_cached_explanation(self, concept_id: str) -> Optional[Dict[str, Any]]:
569
+ """Get cached concept explanation if it exists"""
570
+ return self.concepts_explanations.get(concept_id)
571
+
572
+ def cache_concept_expansion(self, concept_id: str, expansion_data: Dict[str, Any]):
573
+ """Cache concept expansion data"""
574
+ self.concepts_expansions[concept_id] = expansion_data
575
+
576
+ def get_cached_expansion(self, concept_id: str) -> Optional[Dict[str, Any]]:
577
+ """Get cached concept expansion if it exists"""
578
+ return self.concepts_expansions.get(concept_id)
579
+
580
+ def cache_card_explanation(self, concept_id: str, explanation_text: str):
581
+ """缓存卡片点击的解释内容"""
582
+ self.card_explanations[concept_id] = explanation_text
583
+
584
+ def get_cached_card_explanation(self, concept_id: str) -> Optional[str]:
585
+ """获取缓存的卡片解释内容"""
586
+ return self.card_explanations.get(concept_id)
587
+
588
+ # Initialize application state
589
+ app_state = AppState()
590
+
591
+ # CreateFastAPI应用
592
+ app = FastAPI()
593
+
594
+ # 修改 FastAPI 路由部分
595
+ @app.post("/trigger_llm")
596
+ async def trigger_llm(data: dict):
597
+ try:
598
+ concept_id = data.get("concept_id")
599
+ if not concept_id:
600
+ return JSONResponse({"error": "Missing concept_id"}, status_code=400)
601
+
602
+ # 生成解释内容
603
+ explanation_content = generate_card_explanation(concept_id)
604
+
605
+ # 只返回生成的内容,让前端处理UI更新
606
+ return JSONResponse({
607
+ "status": "success",
608
+ "content": explanation_content
609
+ })
610
+
611
+ except Exception as e:
612
+ return JSONResponse({"error": str(e)}, status_code=500)
613
+
614
+ # Helper function for formatting concept cards
615
+ def generate_concept_cards(concept_map: Dict) -> str:
616
+ """Generate HTML for concept cards with enhanced styling"""
617
+ cards_html = '<div class="concept-cards-container">'
618
+
619
+ for concept in concept_map.get("sub_concepts", []):
620
+ difficulty_class = f"difficulty-{concept.get('difficulty', 'basic')}"
621
+ concept_id = concept['id']
622
+ concept_name = concept['name']
623
+ concept_description = concept['description']
624
+
625
+ # 修改fetch回调部分
626
+ cards_html += f"""
627
+ <div class="concept-card {difficulty_class}"
628
+ data-concept-id="{concept_id}"
629
+ onclick="(function(id) {{
630
+ console.log('点击概念卡片:', id);
631
+
632
+ // 更新UI状态
633
+ document.querySelectorAll('.concept-card').forEach(card => {{
634
+ card.classList.remove('selected-card');
635
+ if (card.getAttribute('data-concept-id') === id) {{
636
+ card.classList.add('selected-card');
637
+ }}
638
+ }});
639
+
640
+ // 显示加载动画
641
+ const directAnswer = document.querySelector('.answer-box');
642
+ if (directAnswer) {{
643
+ const existingContainers = document.querySelectorAll('.concept-explanation-container');
644
+ existingContainers.forEach(container => container.remove());
645
+
646
+ const loadingContainer = document.createElement('div');
647
+ loadingContainer.className = 'concept-explanation-container';
648
+ loadingContainer.innerHTML = `
649
+ <div class="loading">
650
+ <div class="loading-spinner"></div>
651
+ <div class="loading-text">正在加载概念解释...</div>
652
+ </div>
653
+ `;
654
+ directAnswer.parentNode.insertBefore(loadingContainer, directAnswer.nextSibling);
655
+ }}
656
+
657
+ // 触发Gradio事件以获取缓存或生成新内容
658
+ const cardSelectionInput = document.getElementById('card-selection');
659
+ if (cardSelectionInput) {{
660
+ cardSelectionInput.value = id;
661
+ cardSelectionInput.dispatchEvent(new Event('input', {{ bubbles: true }}));
662
+ }}
663
+
664
+ }})('{concept_id}')">
665
+ <div class="concept-header">
666
+ <h3>{concept_name}</h3>
667
+ <span class="difficulty-badge">{concept.get('difficulty', 'basic')}</span>
668
+ </div>
669
+ <p>{concept_description}</p>
670
+ </div>
671
+ """
672
+
673
+ cards_html += """
674
+ <style>
675
+ .concept-cards-container {
676
+ display: grid;
677
+ grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
678
+ gap: 15px;
679
+ padding: 10px;
680
+ }
681
+
682
+ .concept-card {
683
+ background: white;
684
+ border-radius: 10px;
685
+ padding: 15px;
686
+ cursor: pointer;
687
+ transition: all 0.3s ease;
688
+ border: 1px solid #e9ecef;
689
+ }
690
+
691
+ .concept-header {
692
+ display: flex;
693
+ justify-content: space-between;
694
+ align-items: center;
695
+ margin-bottom: 10px;
696
+ }
697
+
698
+ .difficulty-badge {
699
+ padding: 4px 8px;
700
+ border-radius: 12px;
701
+ font-size: 0.8em;
702
+ font-weight: 500;
703
+ }
704
+
705
+ .difficulty-basic .difficulty-badge {
706
+ background: #e3f2fd;
707
+ color: #1976d2;
708
+ }
709
+
710
+ .difficulty-intermediate .difficulty-badge {
711
+ background: #fff3e0;
712
+ color: #f57c00;
713
+ }
714
+
715
+ .difficulty-advanced .difficulty-badge {
716
+ background: #ffebee;
717
+ color: #d32f2f;
718
+ }
719
+ </style>
720
+ """
721
+
722
+ return cards_html
723
+
724
+ # Function definitions
725
+ def update_profile(grade, subject, needs):
726
+ app_state.update_user_profile(grade, subject, needs)
727
+ return f"*Current user profile: {grade} {subject} student - Learning needs: {needs if needs else 'Not specified'}*"
728
+
729
+ # 添加新的函数用于生成解释
730
+ def generate_explanation(question: str, concept_map: Dict[str, Any], user_profile: Dict[str, str]) -> str:
731
+ """
732
+ Generate explanation for the question using LLM
733
+
734
+ Args:
735
+ question: Original question
736
+ concept_map: Concept map data
737
+ user_profile: User profile information
738
+
739
+ Returns:
740
+ Generated explanation
741
+ """
742
+ system_prompt = """You are an expert educational AI tutor. Please provide a clear and concise answer
743
+ to the student's question, considering their grade level and subject background.
744
+
745
+ Your response must be in JSON format with the following structure:
746
+ {
747
+ "explanation": "Your detailed explanation here"
748
+ }
749
+
750
+ The explanation should be:
751
+ 1. Direct and focused on the question
752
+ 2. Appropriate for the student's level
753
+ 3. Connected to the main concept
754
+ 4. Easy to understand
755
+ """
756
+
757
+ user_prompt = f"""
758
+ Please provide a JSON response explaining the following question:
759
+
760
+ Question: {question}
761
+
762
+ Student Background:
763
+ - Grade Level: {user_profile['grade']}
764
+ - Subject: {user_profile['subject']}
765
+ - Learning Needs: {user_profile.get('needs', 'Not specified')}
766
+
767
+ Main Concept: {concept_map.get('main_concept', '')}
768
+
769
+ Remember to format your response as a JSON object with an "explanation" field.
770
+ """
771
+
772
+ try:
773
+ response = call_llm(system_prompt, user_prompt)
774
+ return response.get("explanation", "No explanation could be generated.")
775
+ except Exception as e:
776
+ if DEBUG_MODE:
777
+ print(f"Error generating explanation: {str(e)}")
778
+ return "Could not generate explanation at this time."
779
+
780
+ # 修改 analyze_question 函数
781
+ def analyze_question(question, grade, subject, learning_needs):
782
+ """
783
+ Analyze question and return results as HTML
784
+
785
+ Returns:
786
+ Tuple of (answer_section, question_answer, concept_graph, concept_cards, concepts_section, error_msg, card_explanation_section)
787
+ """
788
+ try:
789
+ # 首先返回加载状态
790
+ yield (
791
+ gr.update(visible=True), # 显示答案区域
792
+ gr.update(value="<div class='loading'>Analyzing your question...</div>"), # 显示加载信息
793
+ gr.update(value="<div class='loading'>Generating concept map...</div>"), # 显示加载信息
794
+ gr.update(value="<div class='loading'>Preparing concept cards...</div>"), # 显示加载信息
795
+ gr.update(visible=True),
796
+ gr.update(visible=False),
797
+ gr.update(visible=False) # 隐藏卡片解释区域
798
+ )
799
+
800
+ user_profile = {
801
+ "grade": grade,
802
+ "subject": subject,
803
+ "needs": learning_needs
804
+ }
805
+
806
+ concept_map = decompose_concepts(user_profile, question)
807
+
808
+ # 检查是否需要生成解释
809
+ explanation = concept_map.get("Explanation", "").strip()
810
+ if not explanation:
811
+ explanation = generate_explanation(question, concept_map, user_profile)
812
+ concept_map["Explanation"] = explanation
813
+
814
+ # 创建节点字典
815
+ nodes_dict = {
816
+ concept["id"]: {
817
+ "name": concept["name"],
818
+ "description": concept["description"]
819
+ }
820
+ for concept in concept_map["sub_concepts"]
821
+ }
822
+
823
+ # 存储到应用状态
824
+ app_state.set_concepts_data(concept_map, nodes_dict)
825
+
826
+ # 格式化解答HTML
827
+ answer_html = f"""
828
+ <div class="answer-content">
829
+ {concept_map["Explanation"]}
830
+ </div>
831
+ <div class="main-concept">
832
+ <strong>Main Concept:</strong> {concept_map.get("main_concept", "")}
833
+ </div>
834
+ """
835
+
836
+ # 生成可视化图
837
+ graph_data_url = create_network_graph(concept_map)
838
+ graph_html = f"""
839
+ <div class="concept-graph-container">
840
+ <img src="{graph_data_url}" alt="Concept Knowledge Graph" />
841
+ </div>
842
+ """
843
+
844
+ # 生成概念卡片HTML
845
+ cards_html = generate_concept_cards(concept_map)
846
+
847
+ # 返回最终结果
848
+ yield (
849
+ gr.update(visible=True),
850
+ gr.update(value=answer_html),
851
+ gr.update(value=graph_html),
852
+ gr.update(value=cards_html),
853
+ gr.update(visible=True),
854
+ gr.update(visible=False),
855
+ gr.update(visible=False) # 隐藏卡片解释区域
856
+ )
857
+
858
+ except Exception as e:
859
+ if DEBUG_MODE:
860
+ print(f"Error analyzing question: {str(e)}")
861
+ import traceback
862
+ print(traceback.format_exc())
863
+
864
+ yield (
865
+ gr.update(visible=False),
866
+ gr.update(value=""),
867
+ gr.update(value=""),
868
+ gr.update(value=""),
869
+ gr.update(visible=False),
870
+ gr.update(visible=True, value=f"Error: {str(e)}"),
871
+ gr.update(visible=False) # 出错时也隐藏卡片解释区域
872
+ )
873
+
874
+ def format_explanation(explanation_data):
875
+ """
876
+ Format explanation data into HTML
877
+
878
+ Args:
879
+ explanation_data: Dictionary with explanation data
880
+
881
+ Returns:
882
+ Formatted explanation text
883
+ """
884
+ if not explanation_data:
885
+ return "No explanation available"
886
+
887
+ explanation = explanation_data.get("explanation", "No explanation available")
888
+ return explanation
889
+
890
+ def show_concept_explanation(concept_id):
891
+ if not concept_id or concept_id not in app_state.nodes_dict:
892
+ return {
893
+ explanation_section: gr.update(visible=False),
894
+ error_msg: gr.update(visible=True, value="⚠️ Invalid concept ID")
895
+ }
896
+
897
+ # Get concept information
898
+ concept_info = app_state.nodes_dict[concept_id]
899
+ concept_name = concept_info["name"]
900
+ concept_description = concept_info["description"]
901
+
902
+ # First check from cache
903
+ explanation_data = app_state.get_cached_explanation(concept_id)
904
+
905
+ if not explanation_data:
906
+ try:
907
+ # 使用 llm_utils 替代 llm_chain
908
+ user_profile = {
909
+ "grade": app_state.user_profile.get("grade", "High School"),
910
+ "subject": app_state.user_profile.get("subject", "Math"),
911
+ "needs": app_state.user_profile.get("needs", "")
912
+ }
913
+
914
+ explanation_data = get_concept_explanation(
915
+ user_profile,
916
+ concept_id,
917
+ concept_name,
918
+ concept_description
919
+ )
920
+
921
+ # 缓存结果
922
+ app_state.cache_concept_explanation(concept_id, explanation_data)
923
+
924
+ except Exception as e:
925
+ if DEBUG_MODE:
926
+ print(f"Error explaining concept: {str(e)}")
927
+ return {
928
+ explanation_header: f"### {concept_name} Concept Explanation",
929
+ explanation_content: f"Error generating explanation: {str(e)}",
930
+ examples_content: "",
931
+ resources_content: "",
932
+ practice_content: "",
933
+ concepts_section: gr.update(visible=False),
934
+ explanation_section: gr.update(visible=True),
935
+ error_msg: gr.update(visible=False)
936
+ }
937
+
938
+ # 从explanation_data提取和格式化内容
939
+ explanation = explanation_data.get("explanation", "No explanation available")
940
+
941
+ # Format examples
942
+ examples_html = "<div class='examples-container'>"
943
+ for idx, example in enumerate(explanation_data.get("examples", [])):
944
+ examples_html += f"""
945
+ <div class="example-box">
946
+ <h4>Example {idx+1} ({example.get('difficulty', 'Difficulty not specified')})</h4>
947
+ <p><strong>Problem:</strong> {example.get('problem', 'None')}</p>
948
+ <p><strong>Solution:</strong> <pre style="white-space: pre-wrap;">{example.get('solution', 'None')}</pre></p>
949
+ </div>
950
+ """
951
+ examples_html += "</div>"
952
+
953
+ # Format resources
954
+ resources_html = "<div class='resources-container'>"
955
+ if explanation_data.get("resources"):
956
+ for res in explanation_data.get("resources", []):
957
+ link_html = f"<a href='{res.get('link', '#')}' target='_blank'>View Resource</a>" if res.get('link') else ""
958
+ resources_html += f"""
959
+ <div class="resource-item">
960
+ <p><strong>{res.get('type', 'Resource')}:</strong> {res.get('title', 'Unnamed resource')}</p>
961
+ <p>{res.get('description', 'No description')}</p>
962
+ {link_html}
963
+ </div>
964
+ """
965
+ else:
966
+ resources_html += "<p>No related learning resources available</p>"
967
+ resources_html += "</div>"
968
+
969
+ # Format practice problems
970
+ practice_html = "<div class='practice-container'>"
971
+ if explanation_data.get("practice_questions"):
972
+ for idx, question in enumerate(explanation_data.get("practice_questions", [])):
973
+ practice_html += f"""
974
+ <div class="example-box">
975
+ <h4>Practice Problem {idx+1} ({question.get('difficulty', 'Difficulty not specified')})</h4>
976
+ <p><strong>Question:</strong> {question.get('question', 'None')}</p>
977
+ <details>
978
+ <summary>View Answer</summary>
979
+ <p>{question.get('answer', 'None')}</p>
980
+ </details>
981
+ </div>
982
+ """
983
+ else:
984
+ practice_html += "<p>No practice problems available</p>"
985
+ practice_html += "</div>"
986
+
987
+ return {
988
+ explanation_header: f"### {concept_name} Concept Explanation",
989
+ explanation_content: explanation,
990
+ examples_content: examples_html,
991
+ resources_content: resources_html,
992
+ practice_content: practice_html,
993
+ concepts_section: gr.update(visible=False),
994
+ explanation_section: gr.update(visible=True),
995
+ error_msg: gr.update(visible=False)
996
+ }
997
+
998
+ def back_to_concepts():
999
+ return {
1000
+ concepts_section: gr.update(visible=True),
1001
+ explanation_section: gr.update(visible=False)
1002
+ }
1003
+
1004
+ # JS function to handle click events
1005
+ def handle_concept_click(concept_id):
1006
+ if concept_id:
1007
+ return show_concept_explanation(concept_id)
1008
+ return None
1009
+
1010
+ # 添加新函数,用于生成详细的概念解释
1011
+ def generate_card_explanation(concept_id: str) -> str:
1012
+ """生成详细的概念解释
1013
+
1014
+ Args:
1015
+ concept_id: 概念ID
1016
+
1017
+ Returns:
1018
+ HTML格式的解释内容
1019
+ """
1020
+ try:
1021
+ print(f"开始生成概念解释: {concept_id}")
1022
+
1023
+ # 获取概念信息
1024
+ concept_info = app_state.nodes_dict.get(concept_id)
1025
+ if not concept_info:
1026
+ raise ValueError(f"找不到概念信息: {concept_id}")
1027
+
1028
+ concept_name = concept_info["name"]
1029
+ concept_description = concept_info["description"]
1030
+
1031
+ # 获取前置概念
1032
+ prerequisites = []
1033
+ if app_state.current_concepts_data and "relationships" in app_state.current_concepts_data:
1034
+ for rel in app_state.current_concepts_data["relationships"]:
1035
+ if rel.get("target") == concept_id and rel.get("type") == "prerequisite":
1036
+ source_id = rel.get("source")
1037
+ if source_id in app_state.nodes_dict:
1038
+ prerequisites.append(app_state.nodes_dict[source_id]["name"])
1039
+
1040
+ # 修改system_prompt,明确指定所有必需字段
1041
+ system_prompt = """You are an expert educational tutor. Please provide a clear and detailed explanation of the concept based on the student's grade level.
1042
+
1043
+ Your response MUST be in the following JSON format and MUST include ALL of these fields:
1044
+ {
1045
+ "explanation": "Detailed concept explanation",
1046
+ "key_points": ["key point 1", "key point 2", ...],
1047
+ "examples": [
1048
+ {
1049
+ "problem": "Example problem",
1050
+ "solution": "Detailed solution steps",
1051
+ "difficulty": "basic/intermediate/advanced"
1052
+ }
1053
+ ],
1054
+ "practice": [
1055
+ {
1056
+ "question": "Practice question",
1057
+ "answer": "Answer with explanation",
1058
+ "difficulty": "basic/intermediate/advanced"
1059
+ }
1060
+ ],
1061
+ "resources": [
1062
+ {
1063
+ "type": "Video/Article/Interactive/Book",
1064
+ "title": "Resource title",
1065
+ "description": "Brief description of the resource",
1066
+ "link": "Optional URL to the resource"
1067
+ }
1068
+ ]
1069
+ }
1070
+
1071
+ All fields are required. For resources, provide at least one learning resource that would help students understand this concept better.
1072
+
1073
+ Ensure that:
1074
+ 1. The explanation is appropriate for the student's grade level
1075
+ 2. Use appropriate terminology
1076
+ 3. Include specific examples
1077
+ 4. Provide clear solution steps
1078
+ 5. Include relevant learning resources"""
1079
+
1080
+ user_prompt = f"""Please explain this concept and provide ALL required information including explanation, key points, examples, practice questions, and learning resources:
1081
+
1082
+ Concept Name: {concept_name}
1083
+ Concept Description: {concept_description}
1084
+ Prerequisites: {', '.join(prerequisites) if prerequisites else 'None'}
1085
+
1086
+ Student Background:
1087
+ - Grade Level: {app_state.user_profile.get('grade', 'High School')}
1088
+ - Subject: {app_state.user_profile.get('subject', 'Math')}
1089
+ - Learning Needs: {app_state.user_profile.get('needs', 'Comprehensive understanding')}
1090
+
1091
+ Remember to include all required sections in your response."""
1092
+
1093
+ print("正在调用LLM生成解释...") # 添加调试日志
1094
+
1095
+ # 导入并调用LLM
1096
+ from llm_utils import call_llm
1097
+ try:
1098
+ response = call_llm(system_prompt, user_prompt)
1099
+ print("LLM响应:", response) # 添加调试日志
1100
+
1101
+ if not isinstance(response, dict):
1102
+ raise ValueError("LLM返回的响应格式不正确")
1103
+
1104
+ except Exception as llm_error:
1105
+ print(f"调用LLM时出错: {str(llm_error)}")
1106
+ raise
1107
+
1108
+ # 修改formatted_explanation部分
1109
+ formatted_explanation = f"""
1110
+ <div class="card-explanation">
1111
+ <h3>{concept_name}</h3>
1112
+
1113
+ <div class="explanation-section">
1114
+ <h4>📚 Concept Explanation</h4>
1115
+ <div class="content-box">
1116
+ {response.get('explanation', 'No explanation available')}
1117
+ </div>
1118
+ </div>
1119
+
1120
+ <div class="key-points-section">
1121
+ <h4>🎯 Key Points</h4>
1122
+ <ul>
1123
+ {''.join([f'<li>{point}</li>' for point in response.get('key_points', [])])}
1124
+ </ul>
1125
+ </div>
1126
+
1127
+ <div class="examples-section">
1128
+ <h4>📝 Example Analysis</h4>
1129
+ {''.join([
1130
+ f'''
1131
+ <div class="example-box">
1132
+ <div class="example-header">
1133
+ <span class="difficulty-badge">{example.get('difficulty', 'Basic')}</span>
1134
+ </div>
1135
+ <div class="example-problem">
1136
+ <strong>Example:</strong>{example.get('problem', '')}
1137
+ </div>
1138
+ <div class="solution-box">
1139
+ <strong>Solution:</strong>{example.get('solution', '')}
1140
+ </div>
1141
+ </div>
1142
+ '''
1143
+ for example in response.get('examples', [])
1144
+ ])}
1145
+ </div>
1146
+
1147
+ <div class="practice-section">
1148
+ <h4>✍️ Practice Problems</h4>
1149
+ {''.join([
1150
+ f'''
1151
+ <div class="exercise-box">
1152
+ <div class="exercise-header">
1153
+ <span class="difficulty-badge">{practice.get('difficulty', 'Basic')}</span>
1154
+ </div>
1155
+ <div class="question">
1156
+ <strong>Problem:</strong>{practice.get('question', '')}
1157
+ </div>
1158
+ <details class="answer-details">
1159
+ <summary>View Answer</summary>
1160
+ <div class="solution-box">
1161
+ {practice.get('answer', '')}
1162
+ </div>
1163
+ </details>
1164
+ </div>
1165
+ '''
1166
+ for practice in response.get('practice', [])
1167
+ ])}
1168
+ </div>
1169
+
1170
+ <div class="resources-section">
1171
+ <h4>📚 Learning Resources</h4>
1172
+ {''.join([
1173
+ f'''
1174
+ <div class="resource-box">
1175
+ <div class="resource-header">
1176
+ <span class="resource-type">{resource.get('type', 'Resource')}</span>
1177
+ </div>
1178
+ <div class="resource-content">
1179
+ <strong>{resource.get('title', '')}</strong>
1180
+ <p>{resource.get('description', '')}</p>
1181
+ {f'<a href="{resource.get("link")}" target="_blank">View Resource</a>' if resource.get('link') else ''}
1182
+ </div>
1183
+ </div>
1184
+ '''
1185
+ for resource in response.get('resources', [])
1186
+ ])}
1187
+ </div>
1188
+ </div>
1189
+
1190
+ <style>
1191
+ .card-explanation {{
1192
+ padding: 20px;
1193
+ background: white;
1194
+ border-radius: 12px;
1195
+ box-shadow: 0 2px 8px rgba(0,0,0,0.1);
1196
+ }}
1197
+
1198
+ .explanation-section, .key-points-section, .examples-section,
1199
+ .practice-section, .resources-section {{
1200
+ margin-top: 20px;
1201
+ padding: 15px;
1202
+ background: #f8f9fa;
1203
+ border-radius: 8px;
1204
+ }}
1205
+
1206
+ .content-box {{
1207
+ line-height: 1.6;
1208
+ color: #2c3e50;
1209
+ }}
1210
+
1211
+ .example-box, .exercise-box, .resource-box {{
1212
+ background-color: #f1f8ff;
1213
+ border-left: 4px solid #2196f3;
1214
+ padding: 15px;
1215
+ margin: 10px 0;
1216
+ border-radius: 0 8px 8px 0;
1217
+ }}
1218
+
1219
+ .difficulty-badge, .resource-type {{
1220
+ display: inline-block;
1221
+ padding: 4px 8px;
1222
+ border-radius: 12px;
1223
+ font-size: 0.85em;
1224
+ background: #e3f2fd;
1225
+ color: #1976d2;
1226
+ margin-bottom: 10px;
1227
+ }}
1228
+
1229
+ .solution-box {{
1230
+ margin-top: 10px;
1231
+ padding: 10px;
1232
+ background: rgba(52, 152, 219, 0.05);
1233
+ border-radius: 4px;
1234
+ }}
1235
+
1236
+ .answer-details summary {{
1237
+ cursor: pointer;
1238
+ color: #2196f3;
1239
+ margin: 10px 0;
1240
+ }}
1241
+
1242
+ .resource-content a {{
1243
+ display: inline-block;
1244
+ margin-top: 10px;
1245
+ color: #2196f3;
1246
+ text-decoration: none;
1247
+ padding: 5px 10px;
1248
+ border: 1px solid #2196f3;
1249
+ border-radius: 4px;
1250
+ }}
1251
+
1252
+ .resource-content a:hover {{
1253
+ background: #e3f2fd;
1254
+ }}
1255
+ </style>
1256
+ """
1257
+
1258
+ # 缓存结果
1259
+ app_state.cache_card_explanation(concept_id, formatted_explanation)
1260
+ print(f"已缓存概念解释内容")
1261
+
1262
+ return formatted_explanation
1263
+
1264
+ except Exception as e:
1265
+ import traceback
1266
+ error_msg = f"生成解释时出错: {str(e)}"
1267
+ print(error_msg)
1268
+ print(traceback.format_exc())
1269
+ return f"""<div class="error-message">
1270
+ <h3>无法生成详细解释</h3>
1271
+ <p>{error_msg}</p>
1272
+ <h4>基本概念信息:</h4>
1273
+ <p><strong>{concept_name}</strong>: {concept_description}</p>
1274
+ </div>"""
1275
+
1276
+ def handle_card_selection(concept_id: str) -> Dict:
1277
+ """处理卡片选择事件并生成概念解释
1278
+
1279
+ Args:
1280
+ concept_id: 选中的概念ID
1281
+
1282
+ Returns:
1283
+ 包含面板更新和内容的字典
1284
+ """
1285
+ try:
1286
+ print(f"处理卡片选择: {concept_id}")
1287
+
1288
+ # 首先检查缓存
1289
+ cached_explanation = app_state.get_cached_card_explanation(concept_id)
1290
+ if cached_explanation:
1291
+ print("使用缓存的解释内容")
1292
+ # 直接返回缓存的内容,不需要生成加载动画
1293
+ return {
1294
+ concept_detail_panel: gr.update(visible=True),
1295
+ concept_detail_content: gr.update(value=cached_explanation, visible=True)
1296
+ }
1297
+
1298
+ # 获取概念信息
1299
+ if not concept_id or concept_id not in app_state.nodes_dict:
1300
+ raise ValueError(f"无效的概念ID: {concept_id}")
1301
+
1302
+ # 显示加载动画
1303
+ loading_html = """
1304
+ <div class="loading">
1305
+ <div class="loading-spinner"></div>
1306
+ <div class="loading-text">正在生成概念解释...</div>
1307
+ </div>
1308
+ """
1309
+
1310
+ # 先返回加载状态
1311
+ yield {
1312
+ concept_detail_panel: gr.update(visible=True),
1313
+ concept_detail_content: gr.update(value=loading_html, visible=True)
1314
+ }
1315
+
1316
+ print("开始生成新的解释内容")
1317
+ explanation_content = generate_card_explanation(concept_id)
1318
+
1319
+ # 缓存生成的内容
1320
+ app_state.cache_card_explanation(concept_id, explanation_content)
1321
+ print(f"已缓存概念 {concept_id} 的解释内容")
1322
+
1323
+ # 返回生成的内容
1324
+ return {
1325
+ concept_detail_panel: gr.update(visible=True),
1326
+ concept_detail_content: gr.update(value=explanation_content, visible=True)
1327
+ }
1328
+
1329
+ except Exception as e:
1330
+ import traceback
1331
+ print(f"生成解释时出错: {str(e)}")
1332
+ print(traceback.format_exc())
1333
+ error_content = f"""
1334
+ <div class="error-message">
1335
+ <h3>生成解释时出错</h3>
1336
+ <p>{str(e)}</p>
1337
+ </div>
1338
+ """
1339
+ return {
1340
+ concept_detail_panel: gr.update(visible=True),
1341
+ concept_detail_content: gr.update(value=error_content, visible=True)
1342
+ }
1343
+
1344
+ def create_interface():
1345
+ """
1346
+ Create enhanced Gradio interface with better layout and styling
1347
+ """
1348
+ global expanded_concept_section, expanded_concept_name, expanded_concept_description
1349
+ global key_points, examples, misconceptions, learning_tips, close_expanded
1350
+
1351
+ # Custom CSS with improved styling
1352
+ custom_css = """
1353
+ /* Global styles */
1354
+ body {
1355
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Microsoft YaHei", "WenQuanYi Micro Hei", sans-serif;
1356
+ color: #333;
1357
+ background-color: #f7f9fc;
1358
+ }
1359
+
1360
+ /* Section styling */
1361
+ .section {
1362
+ background: white;
1363
+ border-radius: 15px;
1364
+ padding: 20px;
1365
+ margin-bottom: 20px;
1366
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
1367
+ }
1368
+
1369
+ .header-section {
1370
+ background: linear-gradient(135deg, #2193b0, #6dd5ed);
1371
+ color: white;
1372
+ padding: 20px;
1373
+ border-radius: 15px;
1374
+ margin-bottom: 30px;
1375
+ box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
1376
+ }
1377
+
1378
+ .question-section {
1379
+ background: #f8f9fa;
1380
+ }
1381
+
1382
+ /* Concept graph styling */
1383
+ .concept-graph-container {
1384
+ margin: 20px 0;
1385
+ text-align: center;
1386
+ }
1387
+ .concept-graph-container img {
1388
+ max-width: 100%;
1389
+ border-radius: 10px;
1390
+ box-shadow: 0 4px 15px rgba(0,0,0,0.1);
1391
+ }
1392
+
1393
+ /* Button styles */
1394
+ .primary-button {
1395
+ background: linear-gradient(135deg, #3498db, #2980b9);
1396
+ border: none;
1397
+ color: white;
1398
+ padding: 10px 20px;
1399
+ border-radius: 5px;
1400
+ cursor: pointer;
1401
+ box-shadow: 0 4px 6px rgba(52, 152, 219, 0.3);
1402
+ transition: all 0.3s ease;
1403
+ }
1404
+
1405
+ .primary-button:hover {
1406
+ transform: translateY(-2px);
1407
+ box-shadow: 0 6px 8px rgba(0, 0, 0, 0.1);
1408
+ }
1409
+
1410
+ /* Tab styles */
1411
+ .tabs {
1412
+ border-bottom: 2px solid #e0e0e0;
1413
+ }
1414
+
1415
+ .tab-selected {
1416
+ color: #3498db;
1417
+ border-bottom: 2px solid #3498db;
1418
+ }
1419
+
1420
+ /* Error message styling */
1421
+ .error-message {
1422
+ color: #d32f2f;
1423
+ background: #ffebee;
1424
+ padding: 10px;
1425
+ border-radius: 5px;
1426
+ margin: 10px 0;
1427
+ border-left: 4px solid #d32f2f;
1428
+ }
1429
+
1430
+ /* Concept card styles */
1431
+ .concept-card {
1432
+ transition: all 0.3s ease;
1433
+ border: 1px solid #e0e0e0;
1434
+ border-radius: 12px;
1435
+ padding: 16px;
1436
+ margin-bottom: 16px;
1437
+ cursor: pointer;
1438
+ background-color: #fff;
1439
+ box-shadow: 0 2px 5px rgba(0,0,0,0.05);
1440
+ }
1441
+
1442
+ .concept-card:hover {
1443
+ box-shadow: 0 5px 15px rgba(0,0,0,0.1);
1444
+ transform: translateY(-3px);
1445
+ border-color: #bdc3c7;
1446
+ }
1447
+
1448
+ .selected-card {
1449
+ border-color: #3498db;
1450
+ background-color: rgba(52, 152, 219, 0.05);
1451
+ box-shadow: 0 0 0 2px rgba(52, 152, 219, 0.3);
1452
+ }
1453
+ """
1454
+
1455
+ # 自定义 JavaScript - 移动到HTML头部
1456
+ custom_js_html = """
1457
+ <script type="text/javascript">
1458
+ // 确保函数在全局作用域定义
1459
+ window.conceptCardClick = function(conceptId) {
1460
+ console.log('卡片点击:', conceptId);
1461
+
1462
+ // 找到隐藏输入框并更新
1463
+ const cardSelectionInput = document.getElementById('card-selection');
1464
+ if (cardSelectionInput) {
1465
+ cardSelectionInput.value = conceptId;
1466
+ cardSelectionInput.dispatchEvent(new Event('input', { bubbles: true }));
1467
+
1468
+ // 更新选中样式
1469
+ document.querySelectorAll('.concept-card').forEach(card => {
1470
+ card.classList.remove('selected-card');
1471
+ if (card.getAttribute('data-concept-id') === conceptId) {
1472
+ card.classList.add('selected-card');
1473
+ }
1474
+ });
1475
+ }
1476
+ }
1477
+
1478
+ document.addEventListener('DOMContentLoaded', function() {
1479
+ // 增强图像显示
1480
+ const graphContainer = document.getElementById('concept-graph');
1481
+ if (graphContainer) {
1482
+ const observer = new MutationObserver(function(mutations) {
1483
+ mutations.forEach(function(mutation) {
1484
+ if (mutation.addedNodes && mutation.addedNodes.length > 0) {
1485
+ const img = graphContainer.querySelector('img');
1486
+ if (img) {
1487
+ img.style.maxWidth = '100%';
1488
+ img.style.height = 'auto';
1489
+ img.style.borderRadius = '8px';
1490
+ img.style.boxShadow = '0 4px 8px rgba(0,0,0,0.1)';
1491
+ }
1492
+ }
1493
+ });
1494
+ });
1495
+
1496
+ observer.observe(graphContainer, { childList: true, subtree: true });
1497
+ }
1498
+ });
1499
+ </script>
1500
+ """
1501
+
1502
+ with gr.Blocks(css=custom_css, title="Educational LLM Assistant") as demo:
1503
+ # 添加自定义 JavaScript - 确保作为头部内容
1504
+ gr.HTML(custom_js_html, elem_id="custom-js")
1505
+
1506
+ # Header section
1507
+ with gr.Row(elem_classes="header-section"):
1508
+ with gr.Column(scale=2):
1509
+ gr.Markdown("# 🎓 Educational LLM Assistant")
1510
+ gr.Markdown("Interactive Learning Through AI-Powered Concept Breakdown")
1511
+
1512
+ # Main content container
1513
+ with gr.Row():
1514
+ # Left column - Profile and Question
1515
+ with gr.Column(scale=1):
1516
+ # Profile section
1517
+ with gr.Group(elem_classes="section"):
1518
+ gr.Markdown("### 👤 Learning Profile")
1519
+ with gr.Row():
1520
+ grade_input = gr.Dropdown(
1521
+ choices=["Elementary", "Middle School", "High School", "College", "Graduate"],
1522
+ label="Grade Level",
1523
+ value="High School"
1524
+ )
1525
+ subject_input = gr.Dropdown(
1526
+ choices=["Math", "Physics", "Chemistry", "Biology", "Computer Science"],
1527
+ label="Subject",
1528
+ value="Math"
1529
+ )
1530
+ needs_input = gr.TextArea(
1531
+ label="Learning Goals",
1532
+ placeholder="What do you want to achieve?",
1533
+ lines=3
1534
+ )
1535
+ profile_btn = gr.Button("Save Profile", elem_classes="primary-button")
1536
+ profile_status = gr.Markdown("*No profile set*")
1537
+
1538
+ # Question section
1539
+ with gr.Group(elem_classes="section question-section"):
1540
+ gr.Markdown("### ❓ Your Question")
1541
+ question_input = gr.TextArea(
1542
+ label="Enter your question",
1543
+ placeholder="What would you like to learn about?",
1544
+ lines=4
1545
+ )
1546
+ question_submit_btn = gr.Button(
1547
+ "Analyze Question",
1548
+ elem_classes="primary-button"
1549
+ )
1550
+
1551
+ # Answer section
1552
+ with gr.Group(visible=False, elem_classes="section answer-section") as answer_section:
1553
+ gr.Markdown("### 📝 Direct Answer")
1554
+ question_answer = gr.HTML(
1555
+ value="",
1556
+ elem_classes="answer-box"
1557
+ )
1558
+
1559
+ # 新增可视化生成面板
1560
+ with gr.Group(visible=False) as concept_detail_panel:
1561
+ gr.Markdown("### 🎯 概念详解")
1562
+ concept_detail_content = gr.HTML(
1563
+ value="",
1564
+ elem_classes="concept-detail-box"
1565
+ )
1566
+
1567
+ # Right column - Concept Map and Explanation
1568
+ with gr.Column(scale=2):
1569
+ # Concept map section
1570
+ with gr.Group(visible=False, elem_classes="section") as concepts_section:
1571
+ gr.Markdown("### 🔍 Knowledge Map")
1572
+ # 使用HTML代替Plot
1573
+ concept_graph = gr.HTML(
1574
+ label="Concept Graph",
1575
+ elem_id="concept-graph",
1576
+ elem_classes="concept-graph-container"
1577
+ )
1578
+
1579
+ with gr.Row():
1580
+ concept_cards = gr.HTML(
1581
+ label="Related Concepts",
1582
+ elem_classes="concept-cards-area"
1583
+ )
1584
+
1585
+ # Explanation section
1586
+ with gr.Group(visible=False, elem_classes="section") as explanation_section:
1587
+ explanation_header = gr.Markdown("### 📚 Concept Explanation")
1588
+
1589
+ with gr.Tabs(elem_classes="tabs") as explanation_tabs:
1590
+ with gr.TabItem("📖 Explanation", elem_classes="tab-content"):
1591
+ explanation_content = gr.Markdown()
1592
+ with gr.TabItem("📝 Examples", elem_classes="tab-content"):
1593
+ examples_content = gr.HTML()
1594
+ with gr.TabItem("🔖 Resources", elem_classes="tab-content"):
1595
+ resources_content = gr.HTML()
1596
+ with gr.TabItem("✏️ Practice", elem_classes="tab-content"):
1597
+ practice_content = gr.HTML()
1598
+
1599
+ back_btn = gr.Button(
1600
+ "← Back to Concept Map",
1601
+ elem_classes="primary-button"
1602
+ )
1603
+
1604
+ # 添加扩展内容部分
1605
+ with gr.Group(visible=False, elem_classes="section") as expanded_concept_section:
1606
+ gr.Markdown("### 📚 Expanded Concept Details")
1607
+ expanded_concept_name = gr.Markdown("")
1608
+ expanded_concept_description = gr.Markdown("")
1609
+ with gr.Accordion("Key Points", open=True):
1610
+ key_points = gr.Markdown("")
1611
+ with gr.Accordion("Examples", open=True):
1612
+ examples = gr.Markdown("")
1613
+ with gr.Accordion("Common Misconceptions", open=True):
1614
+ misconceptions = gr.Markdown("")
1615
+ with gr.Accordion("Learning Tips", open=True):
1616
+ learning_tips = gr.Markdown("")
1617
+ close_expanded = gr.Button("Back to Concepts", variant="secondary")
1618
+
1619
+ # Error message
1620
+ error_msg = gr.Markdown(visible=False, elem_classes="error-message")
1621
+
1622
+ # 隐藏的概念选择输入框
1623
+ concept_selection = gr.Textbox(visible=False, elem_id="concept-selection")
1624
+
1625
+ # 新增的卡片点击选择输入框
1626
+ card_selection = gr.Textbox(visible=False, elem_id="card-selection")
1627
+
1628
+ # Event bindings
1629
+ profile_btn.click(
1630
+ update_profile,
1631
+ [grade_input, subject_input, needs_input],
1632
+ profile_status
1633
+ )
1634
+
1635
+ question_submit_btn.click(
1636
+ fn=analyze_question,
1637
+ inputs=[question_input, grade_input, subject_input, needs_input],
1638
+ outputs=[
1639
+ answer_section,
1640
+ question_answer,
1641
+ concept_graph,
1642
+ concept_cards,
1643
+ concepts_section,
1644
+ error_msg,
1645
+ # 重置卡片解释部分
1646
+ concept_detail_panel
1647
+ ],
1648
+ api_name=False,
1649
+ show_progress=True,
1650
+ )
1651
+
1652
+ concept_selection.change(
1653
+ fn=handle_concept_click,
1654
+ inputs=[concept_selection],
1655
+ outputs=[
1656
+ explanation_header,
1657
+ explanation_content,
1658
+ examples_content,
1659
+ resources_content,
1660
+ practice_content,
1661
+ concepts_section,
1662
+ explanation_section,
1663
+ error_msg
1664
+ ]
1665
+ )
1666
+
1667
+ # 新增的卡片点击事件处理
1668
+ card_selection.input(
1669
+ fn=handle_card_selection,
1670
+ inputs=[card_selection],
1671
+ outputs=[
1672
+ concept_detail_panel,
1673
+ concept_detail_content
1674
+ ],
1675
+ api_name="handle_card_click"
1676
+ )
1677
+
1678
+ back_btn.click(
1679
+ fn=back_to_concepts,
1680
+ inputs=None,
1681
+ outputs=[concepts_section, explanation_section]
1682
+ )
1683
+
1684
+ close_expanded.click(
1685
+ fn=lambda: {
1686
+ expanded_concept_section: gr.update(visible=False),
1687
+ concepts_section: gr.update(visible=True)
1688
+ },
1689
+ inputs=[],
1690
+ outputs=[expanded_concept_section, concepts_section]
1691
+ )
1692
+
1693
+ # 添加FastAPI集成
1694
+ gr.mount_gradio_app(app, demo, path="/")
1695
+
1696
+ return demo
1697
+
1698
+ if __name__ == "__main__":
1699
+ demo = create_interface()
1700
+ # 使用uvicorn启动
1701
+ import uvicorn
1702
+ uvicorn.run(app, host="0.0.0.0", port=7861)