-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathapp.py
More file actions
236 lines (205 loc) · 9.5 KB
/
Copy pathapp.py
File metadata and controls
236 lines (205 loc) · 9.5 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
import streamlit as st
from dotenv import load_dotenv
from langchain_groq import ChatGroq
from corpus import load_collection
from graph.workflow import build_nodes
from graph.selection import pick_topic_for_auto_mode
from memory import store
from report import build_session_report
load_dotenv()
# ── Page config ───────────────────────────────────────────
st.set_page_config(page_title="☕ Java Interview Coach", page_icon="☕")
st.title("☕ Java Interview Coach")
st.caption("Practice Java interview questions with AI feedback")
# ── LLM ──────────────────────────────────────────────────
llm = ChatGroq(model="llama-3.3-70b-versatile")
# ── ChromaDB setup (RAG) ────────────────────────────────────
# The actual load/embed logic lives in corpus.py (shared with cli.py);
# this just adds Streamlit's process-lifetime caching on top of it.
load_vector_db = st.cache_resource(load_collection)
collection = load_vector_db()
# ── LangGraph nodes (RAG retrieval + adaptive selection -> evaluate -> hint) ──
@st.cache_resource
def get_nodes(_collection, _llm):
return build_nodes(_collection, _llm)
nodes = get_nodes(collection, llm)
AUTO_TOPIC_LABEL = "🎯 Auto (focus on my weak topics)"
DUE_REVIEW_LABEL = "🔁 Due for Review"
TOPICS = [
"OOP", "Java Core", "Java Collections", "Spring",
"JVM", "Multithreading", "Databases", "Java 8",
"Patterns", "Testing"
]
RATING_LABELS = {
"Again": "🔴 Again",
"Hard": "🟠 Hard",
"Good": "🟢 Good",
"Easy": "🔵 Easy",
}
# ── Session state ─────────────────────────────────────────
if "session_id" not in st.session_state:
st.session_state.session_id = store.start_session()
if "question" not in st.session_state:
st.session_state.question = ""
if "active_topic" not in st.session_state:
st.session_state.active_topic = ""
if "feedback" not in st.session_state:
st.session_state.feedback = ""
if "hint" not in st.session_state:
st.session_state.hint = ""
if "score" not in st.session_state:
st.session_state.score = 0
if "total" not in st.session_state:
st.session_state.total = 0
if "weak_topics" not in st.session_state:
st.session_state.weak_topics = []
if "rated" not in st.session_state:
st.session_state.rated = False
if "next_review_at" not in st.session_state:
st.session_state.next_review_at = ""
def _next_question(topic_choice: str) -> tuple[str, str] | None:
"""Resolve a (question, topic) pair for the chosen mode.
Returns ``None`` for 'Due for Review' mode when nothing is due yet —
callers should show a message instead of clearing the current question.
"""
if topic_choice == DUE_REVIEW_LABEL:
due = store.get_due_questions(db_path=store.DB_PATH, limit=1)
if not due:
return None
return due[0]["question"], due[0]["topic"]
topic = (
pick_topic_for_auto_mode(TOPICS, db_path=store.DB_PATH)
if topic_choice == AUTO_TOPIC_LABEL
else topic_choice
)
state = {"topic": topic, "total_questions": st.session_state.total}
result = nodes["ask"](state)
return result["current_question"], topic
# ── UI ────────────────────────────────────────────────────
topic_choice = st.selectbox(
"Choose a topic:", [AUTO_TOPIC_LABEL, DUE_REVIEW_LABEL] + TOPICS
)
if topic_choice == DUE_REVIEW_LABEL:
st.caption(
"🔁 Resurfaces questions you've previously rated, once their spaced-repetition "
"schedule says they're due (Again: 1 day, Hard: 3 days, Good: 7 days, Easy: 14 days)."
)
else:
st.caption(
"💡 Question difficulty adapts to your saved accuracy on this topic — "
"it gets harder as you improve, and eases up while you're still shaky."
)
if st.button("🎯 Generate Question"):
with st.spinner("Searching question bank..."):
picked = _next_question(topic_choice)
if picked is None:
st.session_state.question = ""
st.session_state.active_topic = ""
st.info("🎉 No questions are due for review right now — check back later!")
else:
st.session_state.question, st.session_state.active_topic = picked
st.session_state.feedback = ""
st.session_state.hint = ""
st.session_state.rated = False
if st.session_state.question:
st.markdown("---")
if st.session_state.active_topic:
st.caption(f"Topic: {st.session_state.active_topic}")
st.markdown(f"### 📌 Question:\n{st.session_state.question}")
answer = st.text_area("Your answer:", height=100)
col1, col2 = st.columns(2)
with col1:
if st.button("✅ Submit Answer"):
if answer.strip():
with st.spinner("Evaluating..."):
state = {
"topic": st.session_state.active_topic,
"current_question": st.session_state.question,
"user_answer": answer,
"score": st.session_state.score,
"weak_topics": st.session_state.weak_topics,
"session_id": st.session_state.session_id,
}
result = nodes["evaluate"](state)
st.session_state.feedback = result["feedback"]
st.session_state.score = result["score"]
st.session_state.weak_topics = result["weak_topics"]
st.session_state.total += 1
else:
st.warning("Please type an answer first!")
with col2:
if st.button("💡 Get Hint"):
with st.spinner("Getting hint..."):
state = {"current_question": st.session_state.question}
result = nodes["hint"](state)
st.session_state.hint = result["hint"]
if st.session_state.hint:
st.info(f"💡 **Hint:** {st.session_state.hint}")
if st.session_state.feedback:
if st.session_state.feedback.strip().upper().startswith("CORRECT"):
st.success(st.session_state.feedback)
else:
st.error(st.session_state.feedback)
st.markdown("**How well did you know this?** _(schedules it for a future review)_")
if st.session_state.rated:
st.caption(f"✅ Rated — next review: {st.session_state.next_review_at[:10]}")
else:
rating_cols = st.columns(4)
for col, (rating, label) in zip(rating_cols, RATING_LABELS.items()):
with col:
if st.button(label, key=f"rate_{rating}"):
next_review_at = store.record_review(
topic=st.session_state.active_topic,
question=st.session_state.question,
rating=rating,
db_path=store.DB_PATH,
)
st.session_state.rated = True
st.session_state.next_review_at = next_review_at
st.rerun()
if st.button("➡️ Next Question"):
with st.spinner("Searching next question..."):
picked = _next_question(topic_choice)
if picked is None:
st.session_state.question = ""
st.session_state.active_topic = ""
st.info("🎉 No questions are due for review right now — check back later!")
else:
st.session_state.question, st.session_state.active_topic = picked
st.session_state.feedback = ""
st.session_state.hint = ""
st.session_state.rated = False
st.rerun()
# ── Sidebar ───────────────────────────────────────────────
with st.sidebar:
st.markdown("## 📊 This Session")
st.metric("Score", f"{st.session_state.score}/{st.session_state.total}")
if st.session_state.weak_topics:
st.markdown("### 📚 Topics to Review")
for t in set(st.session_state.weak_topics):
st.markdown(f"- {t}")
st.markdown("---")
st.markdown("## 📈 All-Time Progress")
cumulative = store.get_cumulative_stats()
st.caption(
f"You've answered **{cumulative['total_questions']}** questions "
f"across **{cumulative['total_sessions']}** session"
f"{'s' if cumulative['total_sessions'] != 1 else ''}."
)
if cumulative["weakest_topics"]:
st.markdown("**Weakest topics overall:**")
for t in cumulative["weakest_topics"]:
s = cumulative["topic_stats"][t]
st.markdown(f"- {t} — {s['correct']}/{s['total']} correct")
if cumulative["total_questions"] > 0:
st.download_button(
"📄 Export Session Report",
data=build_session_report(st.session_state.session_id),
file_name="interview_session_report.md",
mime="text/markdown",
)
st.markdown("---")
if st.button("🔄 Reset Session"):
for key in list(st.session_state.keys()):
del st.session_state[key]
st.rerun()