ketannnn commited on
Commit
f25bf36
·
1 Parent(s): 35299d9

feat: updated pipeline to support matching against multiple JDs sequentially

Browse files
Files changed (1) hide show
  1. frontend/src/app/pipeline/page.tsx +111 -45
frontend/src/app/pipeline/page.tsx CHANGED
@@ -4,18 +4,23 @@ import Link from "next/link";
4
  import { useRouter } from "next/navigation";
5
  import { api } from "../../lib/api";
6
 
 
 
 
 
 
7
  interface PipelineState {
8
  status: "idle" | "uploading" | "embedding" | "matching" | "complete";
9
  sessionName: string;
10
- jdTitle: string;
11
- jdId?: string;
12
  sessionId?: string;
13
  taskId?: string;
14
  startTime?: number;
15
  elapsedTime: number;
16
  }
17
 
18
- const DEFAULT_STATE: PipelineState = { status: "idle", sessionName: "", jdTitle: "", elapsedTime: 0 };
19
 
20
  export default function PipelinePage() {
21
  const router = useRouter();
@@ -31,8 +36,7 @@ export default function PipelinePage() {
31
 
32
  // Inputs
33
  const [sessionName, setSessionName] = useState("");
34
- const [jdTitle, setJdTitle] = useState("");
35
- const [jdDesc, setJdDesc] = useState("");
36
  const [file, setFile] = useState<File | null>(null);
37
 
38
  // Architecture state
@@ -51,7 +55,7 @@ export default function PipelinePage() {
51
  setState(p);
52
  // Resume pipeline loop
53
  if (p.status === "embedding" && p.taskId) pollEmbedding(p.taskId, p);
54
- if (p.status === "matching" && p.jdId && p.sessionId) runMatch(p.jdId, p.sessionId, p);
55
  }
56
  } catch (e) {}
57
  }
@@ -85,31 +89,49 @@ export default function PipelinePage() {
85
  });
86
  };
87
 
 
 
 
 
 
 
 
 
 
 
 
 
 
88
  const startPipeline = async () => {
89
- if (!sessionName || !jdTitle || !jdDesc || !file) {
90
- setError("Please fill out all fields and select a file.");
 
 
 
 
91
  return;
92
  }
93
  setError(null);
94
  const start = Date.now();
95
- updateState({ status: "uploading", sessionName, jdTitle, startTime: start, elapsedTime: 0 });
96
 
97
  try {
98
- // 1. Create Session & JD parallel
99
- const [session, jd] = await Promise.all([
100
- api.createSession(sessionName, "Generated by Auto-Pipeline"),
101
- api.createJD(jdTitle, jdDesc)
102
- ]);
 
103
 
104
- updateState({ sessionId: session.id, jdId: jd.id });
105
 
106
  // 2. Upload file
107
- const uploadRes = await api.uploadCandidates(file, session.id);
108
 
109
  updateState({ status: "embedding", taskId: uploadRes.task_id });
110
 
111
  // 3. Poll embedding
112
- pollEmbedding(uploadRes.task_id, { ...state, status: "embedding", sessionId: session.id, jdId: jd.id, startTime: start });
113
 
114
  } catch (e: any) {
115
  setError("Pipeline failed: " + e.message);
@@ -124,7 +146,7 @@ export default function PipelinePage() {
124
  if (s.status === "SUCCESS") {
125
  clearInterval(poll);
126
  updateState({ status: "matching" });
127
- runMatch(currentState.jdId!, currentState.sessionId!, currentState);
128
  } else if (s.status === "FAILURE") {
129
  clearInterval(poll);
130
  setError("Vector embedding failed.");
@@ -136,9 +158,10 @@ export default function PipelinePage() {
136
  }, 3000);
137
  };
138
 
139
- const runMatch = async (jdId: string, sessionId: string, currentState: PipelineState) => {
140
  try {
141
- await api.triggerMatch(jdId, sessionId);
 
142
  updateState({ status: "complete" });
143
  } catch (e: any) {
144
  setError("Matching failed: " + e.message);
@@ -158,7 +181,7 @@ export default function PipelinePage() {
158
  <div className="max-w-4xl mx-auto px-6 py-12">
159
  <div className="text-center mb-10">
160
  <h1 className="text-3xl font-extrabold tracking-tight mb-3">Universal Run Pipeline</h1>
161
- <p className="text-[var(--color-muted)] text-sm">One-way sequential ingestion, mapping, and reranking hook.</p>
162
  </div>
163
 
164
  {/* STEPPER UI */}
@@ -208,26 +231,11 @@ export default function PipelinePage() {
208
 
209
  {state.status === "idle" ? (
210
  <div className="bg-[var(--color-card)] border border-[var(--color-border)] rounded-2xl p-8 shadow-xl shadow-black/5">
211
- <div className="grid grid-cols-2 gap-6 mb-6">
212
- <div>
213
- <label className="block text-xs font-medium text-[var(--color-muted)] mb-2">Session Name</label>
214
- <input type="text" placeholder="e.g. Q3 Engineering Batch (100k)"
215
- className="w-full bg-[var(--color-surface-2)] border border-[var(--color-border-strong)] rounded-xl px-4 py-3 text-sm outline-none focus:border-[var(--color-brand)] transition-all"
216
- value={sessionName} onChange={e => setSessionName(e.target.value)} />
217
- </div>
218
- <div>
219
- <label className="block text-xs font-medium text-[var(--color-muted)] mb-2">Job Title</label>
220
- <input type="text" placeholder="e.g. Senior Backend Engineer"
221
- className="w-full bg-[var(--color-surface-2)] border border-[var(--color-border-strong)] rounded-xl px-4 py-3 text-sm outline-none focus:border-[var(--color-brand)] transition-all"
222
- value={jdTitle} onChange={e => setJdTitle(e.target.value)} />
223
- </div>
224
- </div>
225
-
226
  <div className="mb-6">
227
- <label className="block text-xs font-medium text-[var(--color-muted)] mb-2">Job Description</label>
228
- <textarea placeholder="Paste the full job description here..." rows={4}
229
- className="w-full bg-[var(--color-surface-2)] border border-[var(--color-border-strong)] rounded-xl px-4 py-3 text-sm outline-none focus:border-[var(--color-brand)] transition-all resize-none"
230
- value={jdDesc} onChange={e => setJdDesc(e.target.value)} />
231
  </div>
232
 
233
  <div className="mb-8">
@@ -237,6 +245,45 @@ export default function PipelinePage() {
237
  onChange={e => setFile(e.target.files?.[0] || null)} />
238
  </div>
239
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
240
  <button onClick={startPipeline}
241
  className="w-full py-4 rounded-xl bg-[var(--color-brand)] text-white font-bold tracking-wide shadow-lg shadow-[var(--color-brand-glow)] hover:brightness-110 transition-all active:scale-[0.98]">
242
  START AUTOMATED PIPELINE
@@ -247,11 +294,30 @@ export default function PipelinePage() {
247
  <div className="text-6xl mb-4">🎉</div>
248
  <h2 className="text-2xl font-bold mb-2">Automated Inference Complete!</h2>
249
  <p className="text-[var(--color-muted)] mb-8 max-w-sm mx-auto">
250
- 100% of candidate logic calculated safely. The background worker is aggressively pulling LLM explanations for the top 60 right now.
251
  </p>
252
- <button onClick={() => router.push(`/jds/${state.jdId}?session_id=${state.sessionId}`)}
253
- className="px-8 py-3 rounded-xl bg-[var(--color-brand)] text-white font-semibold hover:brightness-110 transition-all shadow-lg shadow-[var(--color-brand-glow)]">
254
- View Ranked Candidates
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
255
  </button>
256
  </div>
257
  ) : (
@@ -260,7 +326,7 @@ export default function PipelinePage() {
260
  <h2 className="text-xl font-semibold mb-2">
261
  {state.status === "uploading" ? "Broadcasting to Postgres DB..."
262
  : state.status === "embedding" ? "Running Core CPU Vector Space Projection..."
263
- : "Executing Dual-Stage Neural Match..."}
264
  </h2>
265
  <p className="text-[var(--color-dimmer)] text-sm">
266
  Do not close this tab. The timer will automatically pause and redirect upon completion.
 
4
  import { useRouter } from "next/navigation";
5
  import { api } from "../../lib/api";
6
 
7
+ interface JDInput {
8
+ title: string;
9
+ desc: string;
10
+ }
11
+
12
  interface PipelineState {
13
  status: "idle" | "uploading" | "embedding" | "matching" | "complete";
14
  sessionName: string;
15
+ jdsInfo: JDInput[];
16
+ jdIds: string[];
17
  sessionId?: string;
18
  taskId?: string;
19
  startTime?: number;
20
  elapsedTime: number;
21
  }
22
 
23
+ const DEFAULT_STATE: PipelineState = { status: "idle", sessionName: "", jdsInfo: [], jdIds: [], elapsedTime: 0 };
24
 
25
  export default function PipelinePage() {
26
  const router = useRouter();
 
36
 
37
  // Inputs
38
  const [sessionName, setSessionName] = useState("");
39
+ const [jds, setJds] = useState<JDInput[]>([{ title: "", desc: "" }]);
 
40
  const [file, setFile] = useState<File | null>(null);
41
 
42
  // Architecture state
 
55
  setState(p);
56
  // Resume pipeline loop
57
  if (p.status === "embedding" && p.taskId) pollEmbedding(p.taskId, p);
58
+ if (p.status === "matching" && p.jdIds.length > 0 && p.sessionId) runMatches(p.jdIds, p.sessionId, p);
59
  }
60
  } catch (e) {}
61
  }
 
89
  });
90
  };
91
 
92
+ const addJd = () => setJds([...jds, { title: "", desc: "" }]);
93
+ const removeJd = (idx: number) => {
94
+ if (jds.length === 1) return;
95
+ const newJds = [...jds];
96
+ newJds.splice(idx, 1);
97
+ setJds(newJds);
98
+ };
99
+ const updateJd = (idx: number, field: keyof JDInput, val: string) => {
100
+ const newJds = [...jds];
101
+ newJds[idx][field] = val;
102
+ setJds(newJds);
103
+ };
104
+
105
  const startPipeline = async () => {
106
+ if (!sessionName || !file) {
107
+ setError("Please provide a Session Name and select a file.");
108
+ return;
109
+ }
110
+ if (jds.some(jd => !jd.title || !jd.desc)) {
111
+ setError("Please fill out both Title and Description for all Job Descriptions.");
112
  return;
113
  }
114
  setError(null);
115
  const start = Date.now();
116
+ updateState({ status: "uploading", sessionName, jdsInfo: jds, startTime: start, elapsedTime: 0 });
117
 
118
  try {
119
+ // 1. Create Session & JDs parallel
120
+ const sessionPromise = api.createSession(sessionName, "Generated by Auto-Pipeline");
121
+ const jdPromises = jds.map(jd => api.createJD(jd.title, jd.desc));
122
+
123
+ const [session, ...createdJDs] = await Promise.all([sessionPromise, ...jdPromises]);
124
+ const jdIds = createdJDs.map(j => (j as any).id);
125
 
126
+ updateState({ sessionId: (session as any).id, jdIds });
127
 
128
  // 2. Upload file
129
+ const uploadRes = await api.uploadCandidates(file, (session as any).id);
130
 
131
  updateState({ status: "embedding", taskId: uploadRes.task_id });
132
 
133
  // 3. Poll embedding
134
+ pollEmbedding(uploadRes.task_id, { ...state, status: "embedding", sessionId: (session as any).id, jdIds, startTime: start });
135
 
136
  } catch (e: any) {
137
  setError("Pipeline failed: " + e.message);
 
146
  if (s.status === "SUCCESS") {
147
  clearInterval(poll);
148
  updateState({ status: "matching" });
149
+ runMatches(currentState.jdIds, currentState.sessionId!, currentState);
150
  } else if (s.status === "FAILURE") {
151
  clearInterval(poll);
152
  setError("Vector embedding failed.");
 
158
  }, 3000);
159
  };
160
 
161
+ const runMatches = async (jdIds: string[], sessionId: string, currentState: PipelineState) => {
162
  try {
163
+ // Match each JD against the session in parallel
164
+ await Promise.all(jdIds.map(jdId => api.triggerMatch(jdId, sessionId)));
165
  updateState({ status: "complete" });
166
  } catch (e: any) {
167
  setError("Matching failed: " + e.message);
 
181
  <div className="max-w-4xl mx-auto px-6 py-12">
182
  <div className="text-center mb-10">
183
  <h1 className="text-3xl font-extrabold tracking-tight mb-3">Universal Run Pipeline</h1>
184
+ <p className="text-[var(--color-muted)] text-sm">One-way sequential ingestion, mapping, and reranking hook for multiple JDs.</p>
185
  </div>
186
 
187
  {/* STEPPER UI */}
 
231
 
232
  {state.status === "idle" ? (
233
  <div className="bg-[var(--color-card)] border border-[var(--color-border)] rounded-2xl p-8 shadow-xl shadow-black/5">
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
234
  <div className="mb-6">
235
+ <label className="block text-xs font-medium text-[var(--color-muted)] mb-2">Session Name</label>
236
+ <input type="text" placeholder="e.g. Q3 Engineering Batch (100k)"
237
+ className="w-full bg-[var(--color-surface-2)] border border-[var(--color-border-strong)] rounded-xl px-4 py-3 text-sm outline-none focus:border-[var(--color-brand)] transition-all"
238
+ value={sessionName} onChange={e => setSessionName(e.target.value)} />
239
  </div>
240
 
241
  <div className="mb-8">
 
245
  onChange={e => setFile(e.target.files?.[0] || null)} />
246
  </div>
247
 
248
+ <div className="mb-6 border-t border-[var(--color-border-strong)] pt-6">
249
+ <div className="flex items-center justify-between mb-4">
250
+ <label className="block text-sm font-bold text-[var(--color-text)]">Job Descriptions to Match</label>
251
+ <button
252
+ onClick={addJd}
253
+ className="text-xs px-3 py-1.5 rounded-lg bg-[var(--color-surface-2)] border border-[var(--color-border)] text-[var(--color-muted)] hover:text-[var(--color-text)] transition-colors"
254
+ >
255
+ + Add Another JD
256
+ </button>
257
+ </div>
258
+
259
+ <div className="space-y-6">
260
+ {jds.map((jd, idx) => (
261
+ <div key={idx} className="bg-[var(--color-surface-2)] p-4 rounded-xl border border-[var(--color-border)] relative group">
262
+ {jds.length > 1 && (
263
+ <button
264
+ onClick={() => removeJd(idx)}
265
+ className="absolute -top-2 -right-2 w-6 h-6 rounded-full bg-[var(--color-card)] border border-[var(--color-border-strong)] text-[var(--color-muted)] hover:text-red-400 hover:border-red-400 flex items-center justify-center text-xs opacity-0 group-hover:opacity-100 transition-all z-10"
266
+ >
267
+ ×
268
+ </button>
269
+ )}
270
+ <div className="mb-3">
271
+ <label className="block text-xs font-medium text-[var(--color-muted)] mb-2">JD {idx + 1} Title</label>
272
+ <input type="text" placeholder="e.g. Senior Backend Engineer"
273
+ className="w-full bg-[var(--color-card)] border border-[var(--color-border-strong)] rounded-lg px-3 py-2 text-sm outline-none focus:border-[var(--color-brand)] transition-all"
274
+ value={jd.title} onChange={e => updateJd(idx, "title", e.target.value)} />
275
+ </div>
276
+ <div>
277
+ <label className="block text-xs font-medium text-[var(--color-muted)] mb-2">JD {idx + 1} Description</label>
278
+ <textarea placeholder="Paste the full job description here..." rows={3}
279
+ className="w-full bg-[var(--color-card)] border border-[var(--color-border-strong)] rounded-lg px-3 py-2 text-sm outline-none focus:border-[var(--color-brand)] transition-all resize-none"
280
+ value={jd.desc} onChange={e => updateJd(idx, "desc", e.target.value)} />
281
+ </div>
282
+ </div>
283
+ ))}
284
+ </div>
285
+ </div>
286
+
287
  <button onClick={startPipeline}
288
  className="w-full py-4 rounded-xl bg-[var(--color-brand)] text-white font-bold tracking-wide shadow-lg shadow-[var(--color-brand-glow)] hover:brightness-110 transition-all active:scale-[0.98]">
289
  START AUTOMATED PIPELINE
 
294
  <div className="text-6xl mb-4">🎉</div>
295
  <h2 className="text-2xl font-bold mb-2">Automated Inference Complete!</h2>
296
  <p className="text-[var(--color-muted)] mb-8 max-w-sm mx-auto">
297
+ 100% of candidate logic calculated safely for <strong>{state.jdIds.length}</strong> Job Descriptions. The background worker is aggressively pulling LLM explanations for the top 60 right now.
298
  </p>
299
+
300
+ <div className="max-w-md mx-auto bg-[var(--color-surface-2)] rounded-xl border border-[var(--color-border)] p-4 mb-6">
301
+ <div className="text-xs font-bold text-[var(--color-muted)] uppercase tracking-wider mb-3 text-left">View Matches By JD:</div>
302
+ <div className="space-y-2">
303
+ {state.jdsInfo.map((info, idx) => (
304
+ <Link
305
+ key={idx}
306
+ href={`/jds/${state.jdIds[idx]}?session_id=${state.sessionId}`}
307
+ className="flex justify-between items-center bg-[var(--color-card)] hover:bg-[var(--color-card-hover)] p-3 rounded-lg border border-[var(--color-border-strong)] hover:border-[var(--color-brand)] transition-all group"
308
+ >
309
+ <span className="font-semibold text-sm truncate pr-4">{info.title || `Job Description ${idx + 1}`}</span>
310
+ <span className="text-[10px] px-2 py-1 bg-[var(--color-brand-dim)] text-[var(--color-brand-light)] border border-[var(--color-brand-glow)] rounded-full flex-shrink-0">
311
+ View Ranking →
312
+ </span>
313
+ </Link>
314
+ ))}
315
+ </div>
316
+ </div>
317
+
318
+ <button onClick={() => updateState({ status: "idle", jdsInfo: [{ title: "", desc: "" }], jdIds: [], sessionName: "", startTime: undefined })}
319
+ className="text-xs text-[var(--color-muted)] hover:text-[var(--color-text)] underline underline-offset-2">
320
+ Start a new pipeline run
321
  </button>
322
  </div>
323
  ) : (
 
326
  <h2 className="text-xl font-semibold mb-2">
327
  {state.status === "uploading" ? "Broadcasting to Postgres DB..."
328
  : state.status === "embedding" ? "Running Core CPU Vector Space Projection..."
329
+ : `Executing Dual-Stage Neural Match for ${state.jdIds.length} JDs...`}
330
  </h2>
331
  <p className="text-[var(--color-dimmer)] text-sm">
332
  Do not close this tab. The timer will automatically pause and redirect upon completion.