adeshboudh16 commited on
Commit
c2de869
·
1 Parent(s): 08dfc64

bug fix for dynamic routing logic

Browse files
backend/db/connection.py CHANGED
@@ -21,11 +21,12 @@ async def init_db_pool() -> None:
21
  log.info("Connecting to DB: %s...", db_url[:40])
22
  _pool = await asyncpg.create_pool(
23
  db_url,
24
- min_size=1,
25
  max_size=10,
 
26
  init=_init_connection,
27
  )
28
- log.info("asyncpg pool created (min=1, max=10)")
29
 
30
 
31
  def get_pool() -> asyncpg.Pool:
 
21
  log.info("Connecting to DB: %s...", db_url[:40])
22
  _pool = await asyncpg.create_pool(
23
  db_url,
24
+ min_size=0,
25
  max_size=10,
26
+ max_inactive_connection_lifetime=60, # discard idle connections after 60s (NeonDB suspends at ~300s)
27
  init=_init_connection,
28
  )
29
+ log.info("asyncpg pool created (min=0, max=10, idle_lifetime=60s)")
30
 
31
 
32
  def get_pool() -> asyncpg.Pool:
backend/db/queries.py CHANGED
@@ -251,7 +251,13 @@ async def get_sessions_by_student_id(student_id: str) -> list[asyncpg.Record]:
251
  async def get_session_by_id(session_id: str) -> Optional[asyncpg.Record]:
252
  async with get_pool().acquire() as conn:
253
  return await conn.fetchrow(
254
- "SELECT * FROM interview_sessions WHERE id = $1", session_id
 
 
 
 
 
 
255
  )
256
 
257
 
 
251
  async def get_session_by_id(session_id: str) -> Optional[asyncpg.Record]:
252
  async with get_pool().acquire() as conn:
253
  return await conn.fetchrow(
254
+ """
255
+ SELECT s.*, t.name AS topic_name
256
+ FROM interview_sessions s
257
+ JOIN topics t ON t.id = s.topic_id
258
+ WHERE s.id = $1
259
+ """,
260
+ session_id,
261
  )
262
 
263
 
backend/main.py CHANGED
@@ -49,10 +49,19 @@ app.include_router(interview.router, prefix="/interview")
49
  # React static build — MUST be last (serves the SPA for all non-API routes)
50
  from pathlib import Path
51
  from fastapi.staticfiles import StaticFiles
 
52
 
53
  _dist = Path(__file__).resolve().parent.parent / "frontend" / "dist"
54
  if _dist.is_dir():
55
- app.mount("/", StaticFiles(directory=str(_dist), html=True), name="static")
 
 
 
 
 
 
 
 
56
  log.info("Static files mounted from %s", _dist)
57
  else:
58
  log.warning("frontend/dist not found at %s — SPA will not be served", _dist)
 
49
  # React static build — MUST be last (serves the SPA for all non-API routes)
50
  from pathlib import Path
51
  from fastapi.staticfiles import StaticFiles
52
+ from fastapi.responses import FileResponse
53
 
54
  _dist = Path(__file__).resolve().parent.parent / "frontend" / "dist"
55
  if _dist.is_dir():
56
+ # Catch-all for React Router — serve index.html for any path that isn't a real file
57
+ @app.get("/{full_path:path}", include_in_schema=False)
58
+ async def spa_fallback(full_path: str):
59
+ file = _dist / full_path
60
+ if file.is_file():
61
+ return FileResponse(str(file))
62
+ return FileResponse(str(_dist / "index.html"))
63
+
64
+ app.mount("/", StaticFiles(directory=str(_dist)), name="static")
65
  log.info("Static files mounted from %s", _dist)
66
  else:
67
  log.warning("frontend/dist not found at %s — SPA will not be served", _dist)
backend/prompts.py CHANGED
@@ -100,18 +100,24 @@ def build_report_prompt(
100
 
101
  system = (
102
  "You are an expert technical interview judge. "
103
- "Review the full interview transcript and generate a detailed performance report. "
104
- "Respond with ONLY valid JSON:\n"
 
 
 
 
 
 
105
  '{"score": 0-100, "summary": "2-3 sentence overall assessment", '
106
  '"concept_score": 0-100, "depth_score": 0-100, '
107
- '"mistakes": ["specific mistake 1", ...], "tips": ["actionable improvement 1", ...]}'
108
  )
109
  user = (
110
  f"Topic: {topic}\n"
111
  f"{past_ctx}\n"
112
  f"Questions covered: {', '.join(asked) if asked else 'none'}\n"
113
- f"Identified weak areas: {', '.join(weak_areas) if weak_areas else 'none'}\n\n"
114
  f"Full interview transcript:\n{transcript or summary or 'No transcript available.'}\n\n"
115
- "Judge the student's performance and generate the report JSON."
116
  )
117
  return [{"role": "system", "content": system}, {"role": "user", "content": user}]
 
100
 
101
  system = (
102
  "You are an expert technical interview judge. "
103
+ "Review the full interview transcript and generate a detailed performance report.\n\n"
104
+ "Scoring criteria:\n"
105
+ "- score: overall performance (0-100)\n"
106
+ "- concept_score: did the student correctly understand the underlying concepts and theory? (0-100)\n"
107
+ "- depth_score: did the student give detailed, thorough answers with examples — or just surface-level responses? (0-100)\n"
108
+ "- mistakes: specific factual errors or wrong statements made (list, be concrete)\n"
109
+ "- tips: actionable things the student should study or practise to improve (list)\n\n"
110
+ "Respond with ONLY valid JSON, no markdown:\n"
111
  '{"score": 0-100, "summary": "2-3 sentence overall assessment", '
112
  '"concept_score": 0-100, "depth_score": 0-100, '
113
+ '"mistakes": ["specific mistake 1", ...], "tips": ["actionable tip 1", ...]}'
114
  )
115
  user = (
116
  f"Topic: {topic}\n"
117
  f"{past_ctx}\n"
118
  f"Questions covered: {', '.join(asked) if asked else 'none'}\n"
119
+ f"Identified weak areas during interview: {', '.join(weak_areas) if weak_areas else 'none'}\n\n"
120
  f"Full interview transcript:\n{transcript or summary or 'No transcript available.'}\n\n"
121
+ "Judge the student's performance strictly and generate the report JSON."
122
  )
123
  return [{"role": "system", "content": system}, {"role": "user", "content": user}]
backend/routers/interview.py CHANGED
@@ -158,6 +158,73 @@ async def interview_turn(
158
  return response
159
 
160
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
161
  @router.get("/state/{session_id}")
162
  async def get_interview_state(
163
  session_id: str,
 
158
  return response
159
 
160
 
161
+ @router.post("/finish/{session_id}")
162
+ async def finish_session(
163
+ session_id: str,
164
+ request: Request,
165
+ user: dict = Depends(require_student),
166
+ ):
167
+ """Force-finish an in-progress session and generate a report from whatever answers were given."""
168
+ log.info("POST /finish/%s — user=%s", session_id, user["user_id"])
169
+ session = await queries.get_session_by_id(session_id)
170
+ if not session:
171
+ raise HTTPException(404, "Session not found")
172
+ if str(session["student_id"]) != user["user_id"]:
173
+ raise HTTPException(403, "Access denied")
174
+ if session["status"] == "completed":
175
+ raise HTTPException(400, "Session is already complete")
176
+
177
+ graph = request.app.state.graph
178
+ config = {"configurable": {"thread_id": session_id}}
179
+ checkpoint = await graph.checkpointer.aget(config)
180
+
181
+ if not checkpoint:
182
+ # No checkpoint at all — mark as abandoned with score 0
183
+ abandoned_feedback = {
184
+ "score": 0,
185
+ "summary": "Interview was abandoned before any questions were answered.",
186
+ "concept_score": 0,
187
+ "depth_score": 0,
188
+ "mistakes": [],
189
+ "tips": ["Complete at least one question to receive a score."],
190
+ }
191
+ await queries.update_session_complete(session_id, 0, abandoned_feedback)
192
+ return {"score": 0, "feedback": abandoned_feedback}
193
+
194
+ ch = checkpoint.get("channel_values", {})
195
+ from backend.prompts import build_report_prompt
196
+ from backend.llm import call_llm
197
+ import json as _json
198
+
199
+ prompt = build_report_prompt(
200
+ ch.get("topic_name", ""),
201
+ ch.get("questions_asked", []),
202
+ ch.get("student_weak_areas", []),
203
+ ch.get("conversation_summary", ""),
204
+ ch.get("past_best_score"),
205
+ messages=ch.get("messages", []),
206
+ )
207
+ raw = await call_llm(prompt, max_tokens=400)
208
+
209
+ try:
210
+ feedback = _json.loads(raw)
211
+ score = int(feedback.get("score", 0))
212
+ except (ValueError, _json.JSONDecodeError):
213
+ feedback = {
214
+ "score": 0,
215
+ "summary": raw,
216
+ "concept_score": 0,
217
+ "depth_score": 0,
218
+ "mistakes": [],
219
+ "tips": [],
220
+ }
221
+ score = 0
222
+
223
+ await queries.update_session_complete(session_id, score, feedback)
224
+ log.info("Force-finished session=%s, score=%d", session_id, score)
225
+ return {"score": score, "feedback": feedback}
226
+
227
+
228
  @router.get("/state/{session_id}")
229
  async def get_interview_state(
230
  session_id: str,
backend/routers/sessions.py CHANGED
@@ -17,6 +17,7 @@ async def get_session(session_id: str, user: dict = Depends(get_current_user)):
17
  return {
18
  "id": str(session["id"]),
19
  "topic_id": str(session["topic_id"]),
 
20
  "student_id": str(session["student_id"]),
21
  "status": session["status"],
22
  "score": session["score"],
 
17
  return {
18
  "id": str(session["id"]),
19
  "topic_id": str(session["topic_id"]),
20
+ "topic_name": session["topic_name"],
21
  "student_id": str(session["student_id"]),
22
  "status": session["status"],
23
  "score": session["score"],
docs/question_bank/linear_regression.csv CHANGED
@@ -1,101 +1,18 @@
1
  question_text,difficulty
2
  What is linear regression?,easy
3
- What is the primary goal of a linear regression model?,easy
4
  What is the difference between simple and multiple linear regression?,easy
5
- How do you define the dependent variable in a regression problem?,easy
6
- What are independent variables or features?,easy
7
- What does the slope represent in a simple linear regression equation?,easy
8
- What does the y-intercept represent in a linear model?,easy
9
- Can linear regression be used for classification tasks?,easy
10
- What is a line of best fit?,easy
11
- What is the difference between a parameter and a hyperparameter in this context?,easy
12
- What is an error term or residual?,easy
13
- How do you calculate a residual?,easy
14
- Why do we square the residuals when calculating the cost function?,easy
15
- What is the most common cost function used in linear regression?,easy
16
- What does OLS stand for?,easy
17
- What is the R-squared metric?,easy
18
- What is the range of possible values for R-squared?,easy
19
- Can R-squared ever be negative?,easy
20
- What does an R-squared of 1.0 indicate?,easy
21
- What does an R-squared of 0.0 indicate?,easy
22
- What are the four primary assumptions of linear regression?,medium
23
- What does the assumption of linearity mean?,medium
24
- How can you check if the linearity assumption holds true?,medium
25
- What is homoscedasticity?,medium
26
- What is heteroscedasticity and why is it a problem?,medium
27
- How do you test for homoscedasticity?,medium
28
- What does the assumption of independence of errors mean?,medium
29
- What is autocorrelation?,medium
30
- How can you test for autocorrelation in your residuals?,medium
31
- What does the normality of residuals assumption mean?,medium
32
- Is it necessary for the independent variables to be normally distributed?,medium
33
- How can you verify that your residuals are normally distributed?,medium
34
- What is multicollinearity?,medium
35
- Why is multicollinearity an issue for linear regression?,medium
36
- How do you detect multicollinearity?,medium
37
- What is the Variance Inflation Factor?,medium
38
- What VIF score indicates severe multicollinearity?,medium
39
- How do you resolve high multicollinearity in your dataset?,medium
40
- What is the difference between R-squared and Adjusted R-squared?,medium
41
- Why is Adjusted R-squared preferred when using multiple features?,medium
42
- What is Mean Absolute Error?,medium
43
- What is Mean Squared Error?,medium
44
- What is Root Mean Squared Error?,medium
45
- When would you prefer MAE over RMSE?,medium
46
- When would you prefer RMSE over MAE?,medium
47
- How do outliers affect a linear regression model?,medium
48
- How can you identify outliers in your dataset?,medium
49
- What are leverage points?,medium
50
- What is Cook's Distance used for?,medium
51
- How do you handle categorical variables in linear regression?,medium
52
- What is one-hot encoding?,medium
53
- What is the dummy variable trap?,medium
54
- How do you avoid the dummy variable trap?,medium
55
- Can you use ordinal encoding for linear regression features?,medium
56
- How does feature scaling affect linear regression?,medium
57
- Is feature scaling strictly required for Ordinary Least Squares?,medium
58
- When is feature scaling absolutely necessary for linear regression?,medium
59
- What is a baseline model in regression?,medium
60
- How do you interpret a negative coefficient in your model?,medium
61
- What does a coefficient close to zero imply?,medium
62
- How do you derive the normal equation?,hard
63
- What is the mathematical formula for the normal equation?,hard
64
- What is the computational complexity of the normal equation?,hard
65
- Why might the normal equation fail?,hard
66
- What is a singular matrix and how does it relate to OLS?,hard
67
- How does gradient descent optimize a linear regression model?,hard
68
- What is the learning rate in gradient descent?,hard
69
- What happens if your learning rate is too high?,hard
70
- What happens if your learning rate is too low?,hard
71
- What is the difference between batch and stochastic gradient descent?,hard
72
- What is polynomial regression?,hard
73
- Is polynomial regression a linear or non-linear model?,hard
74
- How do interaction terms work in a regression model?,hard
75
- What is the bias-variance tradeoff?,hard
76
- How does adding more features affect bias and variance?,hard
77
- What is overfitting in the context of linear regression?,hard
78
- How do you detect overfitting?,hard
79
- What is underfitting?,hard
80
- What is regularization?,hard
81
- How does Ridge regression differ from standard linear regression?,hard
82
- What penalty term does Ridge regression use?,hard
83
- How does Lasso regression differ from Ridge regression?,hard
84
- What penalty term does Lasso regression use?,hard
85
- Why does Lasso regression tend to produce sparse models?,hard
86
- What is Elastic Net regularization?,hard
87
- When would you choose Elastic Net over Lasso or Ridge?,hard
88
- How do you tune the regularization hyperparameter?,hard
89
- What is cross-validation and why is it used?,hard
90
- What happens to Ridge coefficients as the penalty term approaches infinity?,hard
91
- What happens to Lasso coefficients as the penalty term approaches infinity?,hard
92
- Can linear regression handle non-linear relationships without polynomial features?,hard
93
- How would you implement gradient descent for linear regression from scratch in Python?,hard
94
- What is the difference between a confidence interval and a prediction interval?,hard
95
- How do you handle missing data before training a linear model?,hard
96
- What is the curse of dimensionality?,hard
97
- How does Principal Component Analysis relate to linear regression?,hard
98
- What is Principal Component Regression?,hard
99
- What are generalized linear models?,hard
100
- How do you interpret the intercept when all features are mean-centered?,hard
101
- What would you do if you have more features than observations?,hard
 
1
  question_text,difficulty
2
  What is linear regression?,easy
 
3
  What is the difference between simple and multiple linear regression?,easy
4
+ What is the primary cost function used in linear regression?,easy
5
+ What do the slope and intercept represent in a simple linear regression model?,easy
6
+ What is the R-squared metric and what does it tell you?,easy
7
+ What are the four core assumptions of linear regression?,medium
8
+ How do you handle categorical variables when building a linear model?,medium
9
+ What is multicollinearity and how can you detect it?,medium
10
+ Explain the difference between R-squared and Adjusted R-squared.,medium
11
+ How does gradient descent work in the context of linear regression?,medium
12
+ What are residuals and why is residual analysis important?,medium
13
+ How do you derive the normal equation for ordinary least squares?,hard
14
+ What are the specific consequences of violating the homoscedasticity assumption?,hard
15
+ Explain the bias-variance tradeoff specifically for linear regression models.,hard
16
+ How do Ridge and Lasso regularization modify standard linear regression?,hard
17
+ How would you implement linear regression from scratch without using external libraries?,hard
18
+ What happens to your regression model when the number of features exceeds the number of observations?,hard
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
frontend/src/api/interview.ts CHANGED
@@ -29,6 +29,17 @@ export async function sendTurn(
29
  return res.json()
30
  }
31
 
 
 
 
 
 
 
 
 
 
 
 
32
  export async function getInterviewState(
33
  sessionId: string,
34
  ): Promise<{ status: string; turn_count: number; last_message: string | null }> {
 
29
  return res.json()
30
  }
31
 
32
+ export async function finishSession(
33
+ sessionId: string,
34
+ ): Promise<{ score: number; feedback: Record<string, unknown> }> {
35
+ const res = await apiFetch(`/interview/finish/${sessionId}`, { method: 'POST' })
36
+ if (!res.ok) {
37
+ const err = await res.json()
38
+ throw new Error(err.detail ?? 'Failed to finish session')
39
+ }
40
+ return res.json()
41
+ }
42
+
43
  export async function getInterviewState(
44
  sessionId: string,
45
  ): Promise<{ status: string; turn_count: number; last_message: string | null }> {
frontend/src/components/student/AttemptRow.tsx CHANGED
@@ -1,8 +1,11 @@
 
1
  import { useNavigate } from 'react-router-dom'
2
  import type { StudentSession } from '../../types'
 
3
 
4
  interface Props {
5
  session: StudentSession
 
6
  }
7
 
8
  function formatDate(iso: string | null): string {
@@ -10,8 +13,10 @@ function formatDate(iso: string | null): string {
10
  return new Date(iso).toLocaleDateString(undefined, { month: 'short', day: 'numeric', year: 'numeric' })
11
  }
12
 
13
- export default function AttemptRow({ session }: Props) {
14
  const navigate = useNavigate()
 
 
15
  const isCompleted = session.status === 'completed'
16
 
17
  const scoreColor =
@@ -23,11 +28,24 @@ export default function AttemptRow({ session }: Props) {
23
  ? 'text-yellow-400'
24
  : 'text-red-400'
25
 
 
 
 
 
 
 
 
 
 
 
 
 
26
  return (
27
  <div className="flex items-center justify-between bg-gray-900 border border-gray-800 rounded-xl px-5 py-4">
28
  <div>
29
  <p className="text-white font-medium">{session.topic_name}</p>
30
  <p className="text-gray-500 text-xs mt-0.5">{formatDate(session.started_at)}</p>
 
31
  </div>
32
  <div className="flex items-center gap-4">
33
  <span className={`text-lg font-semibold ${scoreColor}`}>
@@ -41,9 +59,13 @@ export default function AttemptRow({ session }: Props) {
41
  Report
42
  </button>
43
  ) : (
44
- <span className="text-xs text-yellow-500 bg-yellow-900/30 px-2 py-1 rounded-full">
45
- In progress
46
- </span>
 
 
 
 
47
  )}
48
  </div>
49
  </div>
 
1
+ import { useState } from 'react'
2
  import { useNavigate } from 'react-router-dom'
3
  import type { StudentSession } from '../../types'
4
+ import { finishSession } from '../../api/interview'
5
 
6
  interface Props {
7
  session: StudentSession
8
+ onFinished?: () => void
9
  }
10
 
11
  function formatDate(iso: string | null): string {
 
13
  return new Date(iso).toLocaleDateString(undefined, { month: 'short', day: 'numeric', year: 'numeric' })
14
  }
15
 
16
+ export default function AttemptRow({ session, onFinished }: Props) {
17
  const navigate = useNavigate()
18
+ const [loading, setLoading] = useState(false)
19
+ const [error, setError] = useState('')
20
  const isCompleted = session.status === 'completed'
21
 
22
  const scoreColor =
 
28
  ? 'text-yellow-400'
29
  : 'text-red-400'
30
 
31
+ async function handleGetScore() {
32
+ setLoading(true)
33
+ setError('')
34
+ try {
35
+ await finishSession(session.id)
36
+ onFinished?.()
37
+ } catch (e: unknown) {
38
+ setError(e instanceof Error ? e.message : 'Failed to generate score')
39
+ setLoading(false)
40
+ }
41
+ }
42
+
43
  return (
44
  <div className="flex items-center justify-between bg-gray-900 border border-gray-800 rounded-xl px-5 py-4">
45
  <div>
46
  <p className="text-white font-medium">{session.topic_name}</p>
47
  <p className="text-gray-500 text-xs mt-0.5">{formatDate(session.started_at)}</p>
48
+ {error && <p className="text-red-400 text-xs mt-1">{error}</p>}
49
  </div>
50
  <div className="flex items-center gap-4">
51
  <span className={`text-lg font-semibold ${scoreColor}`}>
 
59
  Report
60
  </button>
61
  ) : (
62
+ <button
63
+ onClick={handleGetScore}
64
+ disabled={loading}
65
+ className="text-xs bg-yellow-600 hover:bg-yellow-500 disabled:opacity-50 disabled:cursor-not-allowed text-white px-3 py-1.5 rounded-lg transition-colors"
66
+ >
67
+ {loading ? 'Scoring…' : 'Get Score'}
68
+ </button>
69
  )}
70
  </div>
71
  </div>
frontend/src/pages/Report.tsx CHANGED
@@ -3,10 +3,35 @@ import { useParams, useNavigate } from 'react-router-dom'
3
  import { getSession } from '../api/sessions'
4
  import type { SessionReport } from '../types'
5
  import ScoreRing from '../components/report/ScoreRing'
6
- import FeedbackCard from '../components/report/FeedbackCard'
7
- import SummaryBlock from '../components/report/SummaryBlock'
8
  import Navbar from '../components/shared/Navbar'
9
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
10
  export default function Report() {
11
  const { sessionId } = useParams<{ sessionId: string }>()
12
  const navigate = useNavigate()
@@ -27,7 +52,7 @@ export default function Report() {
27
  <div className="min-h-screen bg-gray-950">
28
  <Navbar />
29
 
30
- <div className="max-w-2xl mx-auto px-4 py-8 space-y-6">
31
  {loading && (
32
  <div className="space-y-4">
33
  {[...Array(4)].map((_, i) => (
@@ -37,61 +62,91 @@ export default function Report() {
37
  )}
38
 
39
  {error && (
40
- <div className="bg-gray-900 border border-gray-800 rounded-xl p-6 text-center text-red-400 text-sm">
41
  {error}
42
  </div>
43
  )}
44
 
45
- {report && report.feedback && (
46
- <>
47
- {/* Header */}
48
- <div className="bg-gray-900 border border-gray-800 rounded-xl p-6 flex items-center justify-between">
49
- <div>
50
- <p className="text-xs text-gray-500 uppercase tracking-wide mb-1">Interview Report</p>
51
- <h1 className="text-xl font-semibold text-white">Topic {report.topic_id}</h1>
52
- </div>
53
- <ScoreRing score={report.score} />
54
- </div>
55
 
56
- {/* Summary */}
57
- <SummaryBlock summary={report.feedback.summary} />
 
 
 
 
 
 
 
 
 
 
 
 
 
 
58
 
59
- {/* Feedback cards */}
60
- <div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
61
- <FeedbackCard
62
- title="Strengths"
63
- items={report.feedback.tips}
64
- variant="positive"
65
- />
66
- <FeedbackCard
67
- title="Areas to Improve"
68
- items={report.feedback.mistakes}
69
- variant="negative"
70
- />
71
- </div>
72
 
73
- {/* Sub-scores */}
74
- <div className="bg-gray-900 border border-gray-800 rounded-xl p-4 flex gap-6">
75
- <div className="flex-1 text-center">
76
- <p className="text-2xl font-bold text-white">{report.feedback.concept_score}</p>
77
- <p className="text-xs text-gray-500 mt-1">Concept Score</p>
78
- </div>
79
- <div className="w-px bg-gray-800" />
80
- <div className="flex-1 text-center">
81
- <p className="text-2xl font-bold text-white">{report.feedback.depth_score}</p>
82
- <p className="text-xs text-gray-500 mt-1">Depth Score</p>
83
  </div>
84
- </div>
85
 
86
- {/* Back */}
87
- <button
88
- onClick={() => navigate('/student/dashboard')}
89
- className="w-full bg-gray-800 hover:bg-gray-700 text-white rounded-xl px-4 py-3 text-sm font-medium transition-colors"
90
- >
91
- Back to Dashboard
92
- </button>
93
- </>
94
- )}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
95
  </div>
96
  </div>
97
  )
 
3
  import { getSession } from '../api/sessions'
4
  import type { SessionReport } from '../types'
5
  import ScoreRing from '../components/report/ScoreRing'
 
 
6
  import Navbar from '../components/shared/Navbar'
7
 
8
+ function formatDate(iso: string | null): string {
9
+ if (!iso) return '—'
10
+ return new Date(iso).toLocaleDateString(undefined, {
11
+ month: 'long',
12
+ day: 'numeric',
13
+ year: 'numeric',
14
+ })
15
+ }
16
+
17
+ function ScoreBar({ label, value }: { label: string; value: number }) {
18
+ const color = value >= 70 ? 'bg-green-500' : value >= 40 ? 'bg-yellow-500' : 'bg-red-500'
19
+ return (
20
+ <div>
21
+ <div className="flex justify-between mb-1.5">
22
+ <span className="text-xs text-gray-400">{label}</span>
23
+ <span className="text-xs font-semibold text-white">{value}/100</span>
24
+ </div>
25
+ <div className="h-2 bg-gray-800 rounded-full overflow-hidden">
26
+ <div
27
+ className={`h-full rounded-full transition-all duration-700 ${color}`}
28
+ style={{ width: `${value}%` }}
29
+ />
30
+ </div>
31
+ </div>
32
+ )
33
+ }
34
+
35
  export default function Report() {
36
  const { sessionId } = useParams<{ sessionId: string }>()
37
  const navigate = useNavigate()
 
52
  <div className="min-h-screen bg-gray-950">
53
  <Navbar />
54
 
55
+ <div className="max-w-2xl mx-auto px-4 py-8 space-y-5">
56
  {loading && (
57
  <div className="space-y-4">
58
  {[...Array(4)].map((_, i) => (
 
62
  )}
63
 
64
  {error && (
65
+ <div className="bg-gray-900 border border-red-800 rounded-xl p-6 text-center text-red-400 text-sm">
66
  {error}
67
  </div>
68
  )}
69
 
70
+ {report && (() => {
71
+ const fb = report.feedback ?? {}
72
+ const mistakes: string[] = Array.isArray(fb.mistakes) ? fb.mistakes : []
73
+ const tips: string[] = Array.isArray(fb.tips) ? fb.tips : []
74
+ const conceptScore: number = Number(fb.concept_score ?? 0)
75
+ const depthScore: number = Number(fb.depth_score ?? 0)
76
+ const summary: string = fb.summary ?? ''
 
 
 
77
 
78
+ return (
79
+ <>
80
+ {/* Header card */}
81
+ <div className="bg-gray-900 border border-gray-800 rounded-xl p-6">
82
+ <div className="flex items-start justify-between gap-4">
83
+ <div className="flex-1 min-w-0">
84
+ <p className="text-xs text-gray-500 uppercase tracking-widest mb-1">Interview Report</p>
85
+ <h1 className="text-2xl font-bold text-white truncate">{report.topic_name}</h1>
86
+ <p className="text-sm text-gray-500 mt-1">{formatDate(report.completed_at ?? report.started_at)}</p>
87
+ <span className="inline-block mt-3 text-xs font-medium bg-green-900/40 text-green-400 border border-green-800 px-2.5 py-1 rounded-full">
88
+ Completed
89
+ </span>
90
+ </div>
91
+ <ScoreRing score={report.score ?? 0} />
92
+ </div>
93
+ </div>
94
 
95
+ {/* Summary */}
96
+ {summary && (
97
+ <div className="bg-gray-900 border border-gray-800 rounded-xl p-5">
98
+ <h2 className="text-xs font-semibold text-gray-400 uppercase tracking-widest mb-3">Overall Assessment</h2>
99
+ <p className="text-sm text-gray-200 leading-relaxed">{summary}</p>
100
+ </div>
101
+ )}
 
 
 
 
 
 
102
 
103
+ {/* Sub-scores */}
104
+ <div className="bg-gray-900 border border-gray-800 rounded-xl p-5 space-y-4">
105
+ <h2 className="text-xs font-semibold text-gray-400 uppercase tracking-widest">Score Breakdown</h2>
106
+ <ScoreBar label="Concept Understanding" value={conceptScore} />
107
+ <ScoreBar label="Answer Depth" value={depthScore} />
 
 
 
 
 
108
  </div>
 
109
 
110
+ {/* Mistakes */}
111
+ {mistakes.length > 0 && (
112
+ <div className="bg-gray-900 border border-gray-800 border-l-4 border-l-red-500 rounded-xl p-5">
113
+ <h2 className="text-xs font-semibold text-red-400 uppercase tracking-widest mb-3">Mistakes Made</h2>
114
+ <ul className="space-y-2">
115
+ {mistakes.map((item, i) => (
116
+ <li key={i} className="flex gap-2.5 text-sm text-gray-300">
117
+ <span className="text-red-500 mt-0.5 shrink-0">✗</span>
118
+ {item}
119
+ </li>
120
+ ))}
121
+ </ul>
122
+ </div>
123
+ )}
124
+
125
+ {/* Tips */}
126
+ {tips.length > 0 && (
127
+ <div className="bg-gray-900 border border-gray-800 border-l-4 border-l-indigo-500 rounded-xl p-5">
128
+ <h2 className="text-xs font-semibold text-indigo-400 uppercase tracking-widest mb-3">Improvement Tips</h2>
129
+ <ul className="space-y-2">
130
+ {tips.map((item, i) => (
131
+ <li key={i} className="flex gap-2.5 text-sm text-gray-300">
132
+ <span className="text-indigo-400 mt-0.5 shrink-0">→</span>
133
+ {item}
134
+ </li>
135
+ ))}
136
+ </ul>
137
+ </div>
138
+ )}
139
+
140
+ {/* Back */}
141
+ <button
142
+ onClick={() => navigate('/student/dashboard')}
143
+ className="w-full bg-gray-800 hover:bg-gray-700 text-white rounded-xl px-4 py-3 text-sm font-medium transition-colors"
144
+ >
145
+ Back to Dashboard
146
+ </button>
147
+ </>
148
+ )
149
+ })()}
150
  </div>
151
  </div>
152
  )
frontend/src/pages/StudentDashboard.tsx CHANGED
@@ -16,7 +16,8 @@ export default function StudentDashboard() {
16
  const [loading, setLoading] = useState(true)
17
  const [error, setError] = useState('')
18
 
19
- useEffect(() => {
 
20
  Promise.all([getStudentTopics(), getStudentSessions()])
21
  .then(([t, s]) => {
22
  setTopics(t)
@@ -24,7 +25,9 @@ export default function StudentDashboard() {
24
  })
25
  .catch(() => setError('Failed to load dashboard.'))
26
  .finally(() => setLoading(false))
27
- }, [])
 
 
28
 
29
  function handleStartTopic(topic: StudentTopic) {
30
  resetInterview()
@@ -95,7 +98,7 @@ export default function StudentDashboard() {
95
  </h2>
96
  <div className="space-y-3">
97
  {sessions.map((s) => (
98
- <AttemptRow key={s.id} session={s} />
99
  ))}
100
  </div>
101
  </section>
 
16
  const [loading, setLoading] = useState(true)
17
  const [error, setError] = useState('')
18
 
19
+ function loadDashboard() {
20
+ setLoading(true)
21
  Promise.all([getStudentTopics(), getStudentSessions()])
22
  .then(([t, s]) => {
23
  setTopics(t)
 
25
  })
26
  .catch(() => setError('Failed to load dashboard.'))
27
  .finally(() => setLoading(false))
28
+ }
29
+
30
+ useEffect(() => { loadDashboard() }, [])
31
 
32
  function handleStartTopic(topic: StudentTopic) {
33
  resetInterview()
 
98
  </h2>
99
  <div className="space-y-3">
100
  {sessions.map((s) => (
101
+ <AttemptRow key={s.id} session={s} onFinished={loadDashboard} />
102
  ))}
103
  </div>
104
  </section>
frontend/src/types/index.ts CHANGED
@@ -33,10 +33,13 @@ export interface InterviewSession {
33
  export interface SessionReport {
34
  id: string
35
  topic_id: string
 
 
36
  status: string
37
  score: number
38
  feedback: Feedback
39
- messages: Message[]
 
40
  }
41
 
42
  // Student dashboard types
 
33
  export interface SessionReport {
34
  id: string
35
  topic_id: string
36
+ topic_name: string
37
+ student_id: string
38
  status: string
39
  score: number
40
  feedback: Feedback
41
+ started_at: string | null
42
+ completed_at: string | null
43
  }
44
 
45
  // Student dashboard types