ketannnn commited on
Commit
df86e4c
·
1 Parent(s): 15d6f14

fix: Duplicate state Removed from tsx

Browse files
Files changed (1) hide show
  1. frontend/src/app/sessions/[id]/page.tsx +130 -132
frontend/src/app/sessions/[id]/page.tsx CHANGED
@@ -29,7 +29,6 @@ export default function SessionDetailPage({ params }: { params: Promise<{ id: st
29
  const [reranking, setReranking] = useState(false);
30
  const [loading, setLoading] = useState(true);
31
  const [candidateSearch, setCandidateSearch] = useState("");
32
- const [candidateSearch, setCandidateSearch] = useState("");
33
 
34
  const debounce = useCallback(<T extends unknown[]>(fn: (...args: T) => void, ms: number) => {
35
  let t: ReturnType<typeof setTimeout>;
@@ -40,7 +39,7 @@ export default function SessionDetailPage({ params }: { params: Promise<{ id: st
40
  try {
41
  const [s, jList] = await Promise.all([api.getSession(sessionId), api.listJDs()]);
42
  setSession(s);
43
-
44
  // Filter JDs to only those created for this specific session
45
  // The pipeline page stores this mapping in localStorage when it finishes
46
  let filteredJDs = jList as JD[];
@@ -49,9 +48,9 @@ export default function SessionDetailPage({ params }: { params: Promise<{ id: st
49
  if (mapping[sessionId] && Array.isArray(mapping[sessionId]) && mapping[sessionId].length > 0) {
50
  filteredJDs = filteredJDs.filter(jd => mapping[sessionId].includes(jd.id));
51
  }
52
- } catch (e) {}
53
  setJDs(filteredJDs);
54
-
55
  // If a JD is selected, load its match info automatically
56
  if (initialJdId) {
57
  const jdInfo = (jList as JD[]).find(x => x.id === initialJdId);
@@ -60,20 +59,20 @@ export default function SessionDetailPage({ params }: { params: Promise<{ id: st
60
  : DEFAULT_WEIGHTS;
61
 
62
  if (typeof window !== "undefined") {
63
- const stored = localStorage.getItem(`coderound_w_${initialJdId}_${sessionId}`);
64
- if (stored) {
65
- try { initialW = JSON.parse(stored); } catch (e) {}
66
- }
67
  }
68
  setWeights(initialW);
69
-
70
  try {
71
- const r = await api.getMatchResults(initialJdId, sessionId);
72
- setMatch(r);
73
- setBaseResults(r.results);
74
  } catch {
75
- setMatch(null);
76
- setBaseResults(null);
77
  }
78
  }
79
  } catch (e) {
@@ -90,7 +89,7 @@ export default function SessionDetailPage({ params }: { params: Promise<{ id: st
90
  router.replace(`/sessions/${sessionId}?jd_id=${jd.id}`, { scroll: false });
91
  setMatch(null);
92
  setBaseResults(null);
93
-
94
  if (jd.custom_weights && Object.keys(jd.custom_weights).length > 0) {
95
  setWeights(jd.custom_weights as typeof DEFAULT_WEIGHTS);
96
  } else {
@@ -113,7 +112,7 @@ export default function SessionDetailPage({ params }: { params: Promise<{ id: st
113
  if (!selectedJD) return;
114
  setMatching(true);
115
  let attempts = 0;
116
-
117
  const poll = async () => {
118
  try {
119
  const r = await api.triggerMatch(selectedJD, sessionId);
@@ -122,16 +121,16 @@ export default function SessionDetailPage({ params }: { params: Promise<{ id: st
122
  setMatching(false);
123
  } catch (e: any) {
124
  if (e.message === "202_ACCEPTED") {
125
- attempts++;
126
- if (attempts > 20) {
127
- alert("Processing is taking longer than expected. Please check back later.");
128
- setMatching(false);
129
- } else {
130
- setTimeout(poll, 3000);
131
- }
132
  } else {
133
- alert("Matching error: " + e.message);
134
- setMatching(false);
135
  }
136
  }
137
  };
@@ -158,12 +157,12 @@ export default function SessionDetailPage({ params }: { params: Promise<{ id: st
158
  const wNorm: Record<string, number> = totalW > 0 ? Object.fromEntries(Object.entries(nw).map(([k, v]) => [k, v / totalW])) : nw;
159
 
160
  const newRes = baseResults.map(c => {
161
- let score = 0;
162
- const cs = c.component_scores as unknown as Record<string, number>;
163
- ["semantic", "skill", "yoe", "company", "growth", "education"].forEach(k => {
164
- score += (cs[k] || 0) * (wNorm[k] || 0);
165
- });
166
- return { ...c, final_score: score };
167
  });
168
  newRes.sort((a, b) => b.final_score - a.final_score);
169
  newRes.forEach((c, idx) => c.rank = idx + 1);
@@ -214,11 +213,10 @@ export default function SessionDetailPage({ params }: { params: Promise<{ id: st
214
  <button
215
  key={jd.id}
216
  onClick={() => selectJD(jd)}
217
- className={`w-full text-left p-4 rounded-xl border transition-all ${
218
- active
219
- ? "bg-[var(--color-brand-dim)] border-[var(--color-brand-glow)] shadow-[0_0_15px_var(--color-brand-dim)]"
220
  : "bg-[var(--color-card)] border-[var(--color-border)] hover:border-[var(--color-brand-dim)]"
221
- }`}
222
  >
223
  <div className={`font-semibold text-sm ${active ? "text-[var(--color-brand-light)]" : "text-[var(--color-text)]"}`}>{jd.title}</div>
224
  <div className="text-xs text-[var(--color-muted)] mt-1 truncate">Status: {jd.status}</div>
@@ -232,118 +230,118 @@ export default function SessionDetailPage({ params }: { params: Promise<{ id: st
232
  <div className="bg-[var(--color-card)] border border-[var(--color-border)] rounded-2xl p-6 min-h-[500px] shadow-sm">
233
  {!selectedJD ? (
234
  <div className="flex flex-col items-center justify-center h-full text-center opacity-50 py-32">
235
- <div className="text-6xl mb-4">👈</div>
236
- <h3 className="text-xl font-bold">Select a Job Description</h3>
237
- <p className="text-sm mt-2">Filter and rank candidates by choosing a JD from the sidebar.</p>
238
  </div>
239
  ) : matching ? (
240
  <div className="flex flex-col items-center justify-center h-full text-center py-32 animate-fade-in">
241
- <div className="w-12 h-12 border-4 border-[var(--color-brand)] border-t-transparent rounded-full animate-spin mb-4" />
242
- <h3 className="font-semibold text-lg">Running Neural Dual-Stage Match...</h3>
243
- <p className="text-sm text-[var(--color-muted)] mt-1">Sifting through vector embeddings & generating insights.</p>
244
  </div>
245
  ) : !match ? (
246
- <div className="flex flex-col items-center justify-center h-full text-center py-32 animate-fade-in">
247
- <div className="text-5xl mb-4">🎯</div>
248
- <h3 className="text-xl font-bold mb-2">No Match Matrix Found</h3>
249
- <p className="text-sm text-[var(--color-muted)] mb-6 max-w-sm">
250
- You haven't run the candidates against "{currentJdObj?.title}" yet.
251
- </p>
252
- <button onClick={handleMatch} className="px-6 py-3 rounded-xl bg-[var(--color-brand)] text-white font-bold tracking-wide shadow-lg hover:brightness-110 active:scale-95 transition-all">
253
- ⚡ Execute Match Pipeline
254
- </button>
255
- </div>
256
  ) : (
257
  <div className="animate-fade-in">
258
- <div className="flex justify-between items-start mb-8 gap-6 border-b border-[var(--color-border-strong)] pb-6">
259
- <div>
260
- <h2 className="text-2xl font-bold tracking-tight mb-2">Results: {match.jd_title}</h2>
261
- <div className="flex items-center gap-3">
262
- <span className="text-sm text-[var(--color-muted)] px-3 py-1 rounded-full bg-[var(--color-surface-2)] font-medium">
263
- {match.total_matched} candidates ranked
264
- </span>
265
- {reranking && <span className="text-xs text-[var(--color-brand-light)] flex items-center gap-2"><div className="w-3 h-3 border-2 border-[var(--color-brand)] border-t-transparent rounded-full animate-spin" /> Reranking instantly...</span>}
266
- </div>
267
- </div>
268
-
269
- {/* Live Weights */}
270
- <div className="bg-[var(--color-surface-2)] border border-[var(--color-border-strong)] rounded-xl p-4 w-[350px] shadow-inner relative">
271
- <div className="text-[10px] uppercase font-bold text-[var(--color-muted)] mb-3 tracking-wider flex justify-between">
272
- <span>Live Prioritization Weights</span>
273
- <span className="text-[var(--color-brand-light)]">Persists securely</span>
274
- </div>
275
- <div className="grid grid-cols-2 gap-x-4 gap-y-3">
276
- {Object.entries(weights).map(([key, val], i) => (
277
- <div key={key}>
278
- <div className="flex justify-between text-[10px] mb-1">
279
- <span className="text-[var(--color-muted)]">{WEIGHT_LABELS[key]}</span>
280
- <span className="font-semibold">{(val * 100).toFixed(0)}%</span>
281
- </div>
282
- <input
283
- type="range" min={0} max={1} step={0.01} value={val}
284
- style={{ "--thumb-color": SCORE_COLORS[i % SCORE_COLORS.length] } as any}
285
- className="cursor-pointer"
286
- onChange={(e) => {
287
- const nw = { ...weights, [key]: parseFloat(e.target.value) };
288
- if (selectedJD && typeof window !== "undefined") {
289
- localStorage.setItem(`coderound_w_${selectedJD}_${sessionId}`, JSON.stringify(nw));
290
- }
291
- handleWeightChange(key, parseFloat(e.target.value));
292
- }}
293
- />
294
  </div>
295
- ))}
296
- </div>
297
- </div>
298
- </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
299
 
300
- <div className="mb-4">
301
- <input
302
- type="text"
303
- placeholder="Search candidate by name..."
304
- value={candidateSearch}
305
- onChange={e => setCandidateSearch(e.target.value)}
306
- className="w-full bg-[var(--color-surface-2)] border border-[var(--color-border-strong)] focus:border-[var(--color-brand)] rounded-xl px-4 py-2.5 text-sm outline-none transition-all"
307
- />
308
- </div>
309
- <div className="space-y-3">
310
  {match.results
311
  .filter(c => !candidateSearch || (c.name || "").toLowerCase().includes(candidateSearch.toLowerCase()))
312
  .map((c) => (
313
- <Link
314
- key={c.candidate_id}
315
- href={`/jds/${selectedJD}/candidates/${c.candidate_id}?batch=${sessionId}`}
316
- className="flex items-center justify-between p-4 bg-[var(--color-surface-2)] border border-[var(--color-border)] rounded-xl hover:border-[var(--color-brand-light)] hover:bg-[var(--color-card)] hover:shadow-lg transition-all group"
317
- >
318
- <div className="flex items-center gap-5">
319
- <div className={`w-10 h-10 rounded-full flex items-center justify-center text-sm font-bold text-white shadow-sm ${rankClass(c.rank)}`}>
320
- {c.rank}
 
 
 
 
 
 
 
 
321
  </div>
322
- <div>
323
- <div className="font-bold text-sm tracking-wide text-[var(--color-text)] mb-1">{c.name || "Anonymous Candidate"}</div>
324
- <div className="text-xs text-[var(--color-muted)] flex items-center gap-3">
325
- {c.years_of_experience != null && <span className="flex items-center gap-1"><Clock className="w-3.5 h-3.5" /> {c.years_of_experience} yrs</span>}
326
- {c.most_recent_company && <span className="flex items-center gap-1"><Building2 className="w-3.5 h-3.5" /> {c.most_recent_company}</span>}
 
 
 
 
 
 
 
 
327
  </div>
328
  </div>
329
- </div>
330
- <div className="flex items-center gap-8">
331
- <div className="flex gap-1.5">
332
- {c.programming_languages.slice(0, 3).map((l) => (
333
- <span key={l} className="text-[10px] px-2 py-0.5 rounded-full bg-[var(--color-border-strong)] text-[var(--color-text)] border border-[var(--color-border)]">{l}</span>
334
- ))}
335
- {c.programming_languages.length > 3 && <span className="text-[10px] px-2 py-0.5 text-[var(--color-muted)]">+{c.programming_languages.length - 3}</span>}
336
- </div>
337
-
338
- <div className="text-right w-16">
339
- <div className="text-2xl font-bold tracking-tighter" style={{ color: scoreColor(c.final_score) }}>
340
- {(c.final_score * 100).toFixed(0)}
341
- </div>
342
- <div className="text-[9px] uppercase tracking-widest text-[var(--color-dimmer)]">Match</div>
343
- </div>
344
- </div>
345
- </Link>
346
- ))}
347
  </div>
348
  </div>
349
  )}
 
29
  const [reranking, setReranking] = useState(false);
30
  const [loading, setLoading] = useState(true);
31
  const [candidateSearch, setCandidateSearch] = useState("");
 
32
 
33
  const debounce = useCallback(<T extends unknown[]>(fn: (...args: T) => void, ms: number) => {
34
  let t: ReturnType<typeof setTimeout>;
 
39
  try {
40
  const [s, jList] = await Promise.all([api.getSession(sessionId), api.listJDs()]);
41
  setSession(s);
42
+
43
  // Filter JDs to only those created for this specific session
44
  // The pipeline page stores this mapping in localStorage when it finishes
45
  let filteredJDs = jList as JD[];
 
48
  if (mapping[sessionId] && Array.isArray(mapping[sessionId]) && mapping[sessionId].length > 0) {
49
  filteredJDs = filteredJDs.filter(jd => mapping[sessionId].includes(jd.id));
50
  }
51
+ } catch (e) { }
52
  setJDs(filteredJDs);
53
+
54
  // If a JD is selected, load its match info automatically
55
  if (initialJdId) {
56
  const jdInfo = (jList as JD[]).find(x => x.id === initialJdId);
 
59
  : DEFAULT_WEIGHTS;
60
 
61
  if (typeof window !== "undefined") {
62
+ const stored = localStorage.getItem(`coderound_w_${initialJdId}_${sessionId}`);
63
+ if (stored) {
64
+ try { initialW = JSON.parse(stored); } catch (e) { }
65
+ }
66
  }
67
  setWeights(initialW);
68
+
69
  try {
70
+ const r = await api.getMatchResults(initialJdId, sessionId);
71
+ setMatch(r);
72
+ setBaseResults(r.results);
73
  } catch {
74
+ setMatch(null);
75
+ setBaseResults(null);
76
  }
77
  }
78
  } catch (e) {
 
89
  router.replace(`/sessions/${sessionId}?jd_id=${jd.id}`, { scroll: false });
90
  setMatch(null);
91
  setBaseResults(null);
92
+
93
  if (jd.custom_weights && Object.keys(jd.custom_weights).length > 0) {
94
  setWeights(jd.custom_weights as typeof DEFAULT_WEIGHTS);
95
  } else {
 
112
  if (!selectedJD) return;
113
  setMatching(true);
114
  let attempts = 0;
115
+
116
  const poll = async () => {
117
  try {
118
  const r = await api.triggerMatch(selectedJD, sessionId);
 
121
  setMatching(false);
122
  } catch (e: any) {
123
  if (e.message === "202_ACCEPTED") {
124
+ attempts++;
125
+ if (attempts > 20) {
126
+ alert("Processing is taking longer than expected. Please check back later.");
127
+ setMatching(false);
128
+ } else {
129
+ setTimeout(poll, 3000);
130
+ }
131
  } else {
132
+ alert("Matching error: " + e.message);
133
+ setMatching(false);
134
  }
135
  }
136
  };
 
157
  const wNorm: Record<string, number> = totalW > 0 ? Object.fromEntries(Object.entries(nw).map(([k, v]) => [k, v / totalW])) : nw;
158
 
159
  const newRes = baseResults.map(c => {
160
+ let score = 0;
161
+ const cs = c.component_scores as unknown as Record<string, number>;
162
+ ["semantic", "skill", "yoe", "company", "growth", "education"].forEach(k => {
163
+ score += (cs[k] || 0) * (wNorm[k] || 0);
164
+ });
165
+ return { ...c, final_score: score };
166
  });
167
  newRes.sort((a, b) => b.final_score - a.final_score);
168
  newRes.forEach((c, idx) => c.rank = idx + 1);
 
213
  <button
214
  key={jd.id}
215
  onClick={() => selectJD(jd)}
216
+ className={`w-full text-left p-4 rounded-xl border transition-all ${active
217
+ ? "bg-[var(--color-brand-dim)] border-[var(--color-brand-glow)] shadow-[0_0_15px_var(--color-brand-dim)]"
 
218
  : "bg-[var(--color-card)] border-[var(--color-border)] hover:border-[var(--color-brand-dim)]"
219
+ }`}
220
  >
221
  <div className={`font-semibold text-sm ${active ? "text-[var(--color-brand-light)]" : "text-[var(--color-text)]"}`}>{jd.title}</div>
222
  <div className="text-xs text-[var(--color-muted)] mt-1 truncate">Status: {jd.status}</div>
 
230
  <div className="bg-[var(--color-card)] border border-[var(--color-border)] rounded-2xl p-6 min-h-[500px] shadow-sm">
231
  {!selectedJD ? (
232
  <div className="flex flex-col items-center justify-center h-full text-center opacity-50 py-32">
233
+ <div className="text-6xl mb-4">👈</div>
234
+ <h3 className="text-xl font-bold">Select a Job Description</h3>
235
+ <p className="text-sm mt-2">Filter and rank candidates by choosing a JD from the sidebar.</p>
236
  </div>
237
  ) : matching ? (
238
  <div className="flex flex-col items-center justify-center h-full text-center py-32 animate-fade-in">
239
+ <div className="w-12 h-12 border-4 border-[var(--color-brand)] border-t-transparent rounded-full animate-spin mb-4" />
240
+ <h3 className="font-semibold text-lg">Running Neural Dual-Stage Match...</h3>
241
+ <p className="text-sm text-[var(--color-muted)] mt-1">Sifting through vector embeddings & generating insights.</p>
242
  </div>
243
  ) : !match ? (
244
+ <div className="flex flex-col items-center justify-center h-full text-center py-32 animate-fade-in">
245
+ <div className="text-5xl mb-4">🎯</div>
246
+ <h3 className="text-xl font-bold mb-2">No Match Matrix Found</h3>
247
+ <p className="text-sm text-[var(--color-muted)] mb-6 max-w-sm">
248
+ You haven't run the candidates against "{currentJdObj?.title}" yet.
249
+ </p>
250
+ <button onClick={handleMatch} className="px-6 py-3 rounded-xl bg-[var(--color-brand)] text-white font-bold tracking-wide shadow-lg hover:brightness-110 active:scale-95 transition-all">
251
+ ⚡ Execute Match Pipeline
252
+ </button>
253
+ </div>
254
  ) : (
255
  <div className="animate-fade-in">
256
+ <div className="flex justify-between items-start mb-8 gap-6 border-b border-[var(--color-border-strong)] pb-6">
257
+ <div>
258
+ <h2 className="text-2xl font-bold tracking-tight mb-2">Results: {match.jd_title}</h2>
259
+ <div className="flex items-center gap-3">
260
+ <span className="text-sm text-[var(--color-muted)] px-3 py-1 rounded-full bg-[var(--color-surface-2)] font-medium">
261
+ {match.total_matched} candidates ranked
262
+ </span>
263
+ {reranking && <span className="text-xs text-[var(--color-brand-light)] flex items-center gap-2"><div className="w-3 h-3 border-2 border-[var(--color-brand)] border-t-transparent rounded-full animate-spin" /> Reranking instantly...</span>}
264
+ </div>
265
+ </div>
266
+
267
+ {/* Live Weights */}
268
+ <div className="bg-[var(--color-surface-2)] border border-[var(--color-border-strong)] rounded-xl p-4 w-[350px] shadow-inner relative">
269
+ <div className="text-[10px] uppercase font-bold text-[var(--color-muted)] mb-3 tracking-wider flex justify-between">
270
+ <span>Live Prioritization Weights</span>
271
+ <span className="text-[var(--color-brand-light)]">Persists securely</span>
272
+ </div>
273
+ <div className="grid grid-cols-2 gap-x-4 gap-y-3">
274
+ {Object.entries(weights).map(([key, val], i) => (
275
+ <div key={key}>
276
+ <div className="flex justify-between text-[10px] mb-1">
277
+ <span className="text-[var(--color-muted)]">{WEIGHT_LABELS[key]}</span>
278
+ <span className="font-semibold">{(val * 100).toFixed(0)}%</span>
 
 
 
 
 
 
 
 
 
 
 
 
 
279
  </div>
280
+ <input
281
+ type="range" min={0} max={1} step={0.01} value={val}
282
+ style={{ "--thumb-color": SCORE_COLORS[i % SCORE_COLORS.length] } as any}
283
+ className="cursor-pointer"
284
+ onChange={(e) => {
285
+ const nw = { ...weights, [key]: parseFloat(e.target.value) };
286
+ if (selectedJD && typeof window !== "undefined") {
287
+ localStorage.setItem(`coderound_w_${selectedJD}_${sessionId}`, JSON.stringify(nw));
288
+ }
289
+ handleWeightChange(key, parseFloat(e.target.value));
290
+ }}
291
+ />
292
+ </div>
293
+ ))}
294
+ </div>
295
+ </div>
296
+ </div>
297
 
298
+ <div className="mb-4">
299
+ <input
300
+ type="text"
301
+ placeholder="Search candidate by name..."
302
+ value={candidateSearch}
303
+ onChange={e => setCandidateSearch(e.target.value)}
304
+ className="w-full bg-[var(--color-surface-2)] border border-[var(--color-border-strong)] focus:border-[var(--color-brand)] rounded-xl px-4 py-2.5 text-sm outline-none transition-all"
305
+ />
306
+ </div>
307
+ <div className="space-y-3">
308
  {match.results
309
  .filter(c => !candidateSearch || (c.name || "").toLowerCase().includes(candidateSearch.toLowerCase()))
310
  .map((c) => (
311
+ <Link
312
+ key={c.candidate_id}
313
+ href={`/jds/${selectedJD}/candidates/${c.candidate_id}?batch=${sessionId}`}
314
+ className="flex items-center justify-between p-4 bg-[var(--color-surface-2)] border border-[var(--color-border)] rounded-xl hover:border-[var(--color-brand-light)] hover:bg-[var(--color-card)] hover:shadow-lg transition-all group"
315
+ >
316
+ <div className="flex items-center gap-5">
317
+ <div className={`w-10 h-10 rounded-full flex items-center justify-center text-sm font-bold text-white shadow-sm ${rankClass(c.rank)}`}>
318
+ {c.rank}
319
+ </div>
320
+ <div>
321
+ <div className="font-bold text-sm tracking-wide text-[var(--color-text)] mb-1">{c.name || "Anonymous Candidate"}</div>
322
+ <div className="text-xs text-[var(--color-muted)] flex items-center gap-3">
323
+ {c.years_of_experience != null && <span className="flex items-center gap-1"><Clock className="w-3.5 h-3.5" /> {c.years_of_experience} yrs</span>}
324
+ {c.most_recent_company && <span className="flex items-center gap-1"><Building2 className="w-3.5 h-3.5" /> {c.most_recent_company}</span>}
325
+ </div>
326
+ </div>
327
  </div>
328
+ <div className="flex items-center gap-8">
329
+ <div className="flex gap-1.5">
330
+ {c.programming_languages.slice(0, 3).map((l) => (
331
+ <span key={l} className="text-[10px] px-2 py-0.5 rounded-full bg-[var(--color-border-strong)] text-[var(--color-text)] border border-[var(--color-border)]">{l}</span>
332
+ ))}
333
+ {c.programming_languages.length > 3 && <span className="text-[10px] px-2 py-0.5 text-[var(--color-muted)]">+{c.programming_languages.length - 3}</span>}
334
+ </div>
335
+
336
+ <div className="text-right w-16">
337
+ <div className="text-2xl font-bold tracking-tighter" style={{ color: scoreColor(c.final_score) }}>
338
+ {(c.final_score * 100).toFixed(0)}
339
+ </div>
340
+ <div className="text-[9px] uppercase tracking-widest text-[var(--color-dimmer)]">Match</div>
341
  </div>
342
  </div>
343
+ </Link>
344
+ ))}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
345
  </div>
346
  </div>
347
  )}