-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathapp.py
More file actions
295 lines (251 loc) · 12.3 KB
/
Copy pathapp.py
File metadata and controls
295 lines (251 loc) · 12.3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
"""知识图谱学习助手 — Streamlit 前端"""
import streamlit as st
import os
import tempfile
from state import AppState
from kg_engine import build_graph_from_file
from quiz_engine import generate_questions, grade_answer, get_today_review, get_weakness_report
st.set_page_config(page_title="知识图谱学习助手", page_icon="🧠", layout="wide")
# ─── 初始化状态 ───
state = AppState()
state.load()
# 会话状态
if "current_questions" not in st.session_state:
st.session_state.current_questions = []
if "current_idx" not in st.session_state:
st.session_state.current_idx = 0
if "show_result" not in st.session_state:
st.session_state.show_result = {}
# ─── 侧边栏 ───
with st.sidebar:
st.title("🧠 知识图谱学习助手")
st.caption("基于 DeepSeek + SM-2 的智能学习系统")
st.divider()
stats = state.get_stats()
st.metric("概念数", stats["concepts"])
st.metric("关系数", stats["relations"])
st.metric("题目数", stats["questions"])
st.metric("答题记录", stats["records"])
if stats["records"] > 0:
st.metric("正确率", stats["accuracy"])
st.divider()
if st.button("🗑️ 清空所有数据", type="secondary", use_container_width=True):
state.clear()
state.save()
st.session_state.current_questions = []
st.session_state.current_idx = 0
st.session_state.show_result = {}
st.rerun()
# ─── 主区域: 四个 Tab ───
tab1, tab2, tab3, tab4 = st.tabs(["📤 上传笔记", "🕸️ 知识图谱", "📝 答题模式", "📊 错题本"])
# ─── Tab 1: 上传笔记 ───
with tab1:
st.header("上传学习笔记")
st.caption("支持 PDF、Markdown、TXT 格式。AI 将自动抽取知识概念并构建图谱。")
uploaded_file = st.file_uploader("拖拽文件到此处", type=["pdf", "md", "txt"], key="uploader")
if uploaded_file:
# 保存到临时文件
suffix = os.path.splitext(uploaded_file.name)[1]
with tempfile.NamedTemporaryFile(delete=False, suffix=suffix) as tmp:
tmp.write(uploaded_file.getvalue())
tmp_path = tmp.name
if st.button("🚀 开始抽取知识图谱", type="primary", use_container_width=True):
with st.spinner("AI 正在分析文档,抽取概念和关系..."):
result = build_graph_from_file(tmp_path)
if "error" in result:
st.error(result["error"])
else:
st.success(f"抽取完成!新增 {result['new_concepts']} 个概念,总计 {result['concepts']} 个概念、{result['relations']} 条关系。")
st.balloons()
os.unlink(tmp_path)
# 显示已有概念列表
if state.concepts:
st.divider()
st.subheader(f"已收录概念({len(state.concepts)})")
for c in list(state.concepts.values())[:20]:
with st.expander(f"{c.name} [{c.category}] 难度:{'⭐'*c.difficulty}"):
st.write(c.description)
if c.related_ids:
related = [state.concepts[rid].name for rid in c.related_ids if rid in state.concepts]
st.caption(f"关联: {'、'.join(related[:6])}")
else:
st.info("还没有概念数据,请先上传笔记文件。")
# ─── Tab 2: 知识图谱 ───
with tab2:
st.header("知识图谱")
if not state.concepts:
st.info("图谱为空,请先到「上传笔记」页面添加学习资料。")
else:
# 筛选器
col1, col2 = st.columns(2)
with col1:
categories = list(set(c.category for c in state.concepts.values()))
selected_cats = st.multiselect("筛选分类", categories, default=categories, key="graph_filter")
with col2:
difficulty_range = st.slider("难度范围", 1, 5, (1, 5), key="diff_filter")
# 构建PyVis图
graph_data = state.get_graph_data()
filtered_nodes = [
n for n in graph_data["nodes"]
if n["group"] in selected_cats and difficulty_range[0] <= n["difficulty"] <= difficulty_range[1]
]
filtered_ids = {n["id"] for n in filtered_nodes}
filtered_edges = [
e for e in graph_data["edges"]
if e["from"] in filtered_ids and e["to"] in filtered_ids
]
if not filtered_nodes:
st.warning("当前筛选条件下没有匹配的概念。")
else:
# 使用 networkx + pyvis 渲染
try:
from pyvis.network import Network
import networkx as nx
net = Network(height="550px", width="100%", bgcolor="#ffffff", font_color="#333333")
color_map = {
"统计学": "#FF6B6B", "机器学习": "#4ECDC4", "Python": "#45B7D1",
"数据库": "#96CEB4", "业务分析": "#FFEAA7", "未分类": "#DDA0DD",
}
for n in filtered_nodes:
color = color_map.get(n["group"], "#DDA0DD")
net.add_node(
n["id"], label=n["label"], title=n["title"],
color=color, size=max(15, n["value"] * 8),
)
for e in filtered_edges:
net.add_edge(e["from"], e["to"], title=e["title"], label=e["label"])
net.set_options("""
{
"physics": {
"barnesHut": {"gravitationalConstant": -3000, "springLength": 200},
"minVelocity": 0.75
},
"edges": {"smooth": {"type": "curvedCW"}, "arrows": {"to": {"enabled": true}}}
}
""")
net.save_graph("data/graph.html")
with open("data/graph.html", "r", encoding="utf-8") as f:
st.components.v1.html(f.read(), height=550)
except ImportError:
st.warning("需要安装 pyvis 和 networkx: pip install pyvis networkx")
# 概念列表
st.divider()
st.subheader("概念详情")
for c in list(state.concepts.values())[:30]:
if c.category in selected_cats and difficulty_range[0] <= c.difficulty <= difficulty_range[1]:
with st.expander(f"📌 {c.name} [{c.category}] {'⭐'*c.difficulty}"):
st.write(c.description)
st.caption(f"来源: {c.source_file}")
# ─── Tab 3: 答题模式 ───
with tab3:
st.header("答题模式")
if not state.concepts:
st.info("请先上传笔记构建知识图谱。")
else:
# 选题范围
col1, col2, col3 = st.columns(3)
with col1:
cats = list(set(c.category for c in state.concepts.values()))
quiz_cats = st.multiselect("考察分类", cats, default=cats, key="quiz_cats")
with col2:
count = st.number_input("题目数量", 1, 10, 5)
with col3:
q_type = st.selectbox("题型", ["mixed", "choice", "short_answer"], format_func=lambda x: {"mixed": "混合", "choice": "选择题", "short_answer": "简答题"}[x])
if st.button("🎲 生成题目", type="primary", use_container_width=True):
target_ids = [c.id for c in state.concepts.values() if c.category in quiz_cats]
if target_ids:
with st.spinner("AI 正在出题..."):
st.session_state.current_questions = generate_questions(target_ids, count, q_type)
st.session_state.current_idx = 0
st.session_state.show_result = {}
st.rerun()
# 答题区
questions = st.session_state.current_questions
if questions:
idx = st.session_state.current_idx
if idx < len(questions):
q = questions[idx]
st.divider()
type_badge = "🔤 选择题" if q.q_type == "choice" else "✍️ 简答题"
st.subheader(f"第 {idx+1}/{len(questions)} 题 {type_badge} 难度:{'⭐'*q.difficulty}")
# 显示关联知识点
concept_names = [state.concepts[cid].name for cid in q.concept_ids if cid in state.concepts]
st.caption(f"知识点: {'、'.join(concept_names)}")
st.markdown(f"**{q.question}**")
# 答题输入
result_key = f"result_{idx}"
if result_key not in st.session_state.show_result:
if q.q_type == "choice":
user_answer = st.radio("请选择:", q.options, key=f"choice_{idx}")
else:
user_answer = st.text_area("请输入你的答案:", key=f"short_{idx}", height=100)
col_btn1, col_btn2 = st.columns([1, 4])
with col_btn1:
if st.button("✅ 提交", type="primary", key=f"submit_{idx}"):
with st.spinner("AI 正在批改..."):
record = grade_answer(q, user_answer)
st.session_state.show_result[result_key] = record
st.rerun()
else:
# 显示结果
record = st.session_state.show_result[result_key]
if record.is_correct:
st.success(f"✅ 正确!得分: {record.score}/5")
else:
st.error(f"❌ 需要加强。得分: {record.score}/5")
st.info(f"**正确答案:** {q.correct_answer}")
st.markdown(f"**解析:** {q.explanation}")
st.caption(f"**AI 反馈:** {record.feedback}")
st.caption(f"下次复习: {record.next_review}(间隔 {record.sm2_interval} 天)")
# 导航按钮
col_prev, col_next = st.columns(2)
with col_prev:
if idx > 0 and st.button("⬅️ 上一题", key=f"prev_{idx}"):
st.session_state.current_idx -= 1
st.rerun()
with col_next:
if idx < len(questions) - 1 and st.button("下一题 ➡️", key=f"next_{idx}"):
st.session_state.current_idx += 1
st.rerun()
else:
st.success("🎉 全部题目已完成!")
if st.button("再来一组", type="primary"):
st.session_state.current_questions = []
st.session_state.current_idx = 0
st.session_state.show_result = {}
st.rerun()
# ─── Tab 4: 错题本 ───
with tab4:
st.header("错题本 & 薄弱点分析")
if not state.records:
st.info("还没有答题记录,请先到「答题模式」做题。")
else:
# 薄弱知识点
st.subheader("📉 薄弱知识点 Top 10")
weakness = get_weakness_report(10)
if weakness:
for concept, rate in weakness:
st.progress(rate, text=f"{concept.name} [{concept.category}] — 正确率 {rate:.0%}")
else:
st.success("所有知识点掌握良好!")
# 今日待复习
st.divider()
st.subheader("📅 今日待复习")
due_items = get_today_review()
if due_items:
for record, question in due_items[:10]:
with st.expander(f"📌 {question.question[:80]}...(上次得分:{record.score}/5,间隔:{record.sm2_interval}天)"):
st.markdown(f"**上次你的答案:** {record.user_answer}")
st.markdown(f"**正确答案:** {question.correct_answer}")
st.markdown(f"**解析:** {question.explanation}")
st.caption(f"**反馈:** {record.feedback}")
else:
st.success("🎉 今天没有需要复习的题目!")
# 历史记录
st.divider()
st.subheader("📋 全部答题记录")
for r in reversed(state.records[-20:]):
q = state.questions.get(r.question_id)
if q:
icon = "✅" if r.is_correct else "❌"
st.caption(f"{icon} {q.question[:60]}... | 得分:{r.score}/5 | {r.reviewed_at} | 下次:{r.next_review}")