Skip to content

Commit 0289334

Browse files
committed
statefulness
1 parent 7969a51 commit 0289334

File tree

1 file changed

+109
-50
lines changed

1 file changed

+109
-50
lines changed

src/components/interactive/RoleFitIndexForm.tsx

Lines changed: 109 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -30,15 +30,88 @@ type FormValues = z.infer<typeof schema>;
3030

3131
type Me = UserProfile | null;
3232

33+
// State machine configuration
34+
const STATE_CONFIG = {
35+
idle: {
36+
step: 0,
37+
buttonText: "Analyze Role Fit Now",
38+
progressStep: -1,
39+
isProcessing: false,
40+
canSubmit: true,
41+
helperText: null,
42+
isError: false
43+
},
44+
uploading: {
45+
step: 1,
46+
buttonText: "Uploading CV…",
47+
progressStep: 0,
48+
isProcessing: true,
49+
canSubmit: false,
50+
helperText: null,
51+
isError: false
52+
},
53+
saving: {
54+
step: 1,
55+
buttonText: "Saving submission…",
56+
progressStep: 0,
57+
isProcessing: true,
58+
canSubmit: false,
59+
helperText: null,
60+
isError: false
61+
},
62+
submitted: {
63+
step: 2,
64+
buttonText: "Analyzing…",
65+
progressStep: 0,
66+
isProcessing: true,
67+
canSubmit: false,
68+
helperText: "Analyzing your CV & JD — this usually takes ~30 seconds. You'll be redirected when the report is ready.",
69+
isError: false
70+
},
71+
parsed_jd: {
72+
step: 2,
73+
buttonText: "Analyzing…",
74+
progressStep: 1,
75+
isProcessing: true,
76+
canSubmit: false,
77+
helperText: "Analyzing your CV & JD — this usually takes ~30 seconds. You'll be redirected when the report is ready.",
78+
isError: false
79+
},
80+
generated_report: {
81+
step: 2,
82+
buttonText: "Analyzing…",
83+
progressStep: 2,
84+
isProcessing: true,
85+
canSubmit: false,
86+
helperText: "Analyzing your CV & JD — this usually takes ~30 seconds. You'll be redirected when the report is ready.",
87+
isError: false
88+
},
89+
redirecting: {
90+
step: 2,
91+
buttonText: "Analyzing…",
92+
progressStep: 3,
93+
isProcessing: true,
94+
canSubmit: false,
95+
helperText: "Analyzing your CV & JD — this usually takes ~30 seconds. You'll be redirected when the report is ready.",
96+
isError: false
97+
},
98+
failed_parsing_jd: {
99+
step: 2,
100+
buttonText: "Analyze Role Fit Now",
101+
progressStep: 1,
102+
isProcessing: false,
103+
canSubmit: true,
104+
helperText: "Failed to parse the job description. Please check the format and try again.",
105+
isError: true
106+
}
107+
} as const;
108+
109+
type StateKey = keyof typeof STATE_CONFIG;
110+
33111
export default function RoleFitForm() {
34-
const [step, setStep] = useState<0 | 1 | 2>(0); // 0 idle, 1 uploading, 2 analyzing
35-
const [submitting, setSubmitting] = useState(false);
36-
const [buttonText, setButtonText] = useState("Analyze Role Fit Now");
37-
const [error, setError] = useState("");
38-
const [submissionStatus, setSubmissionStatus] = useState<
39-
"submitted" | "parsed_jd" | "generated_report" | "redirecting" | "failed_parsing_jd"
40-
>("submitted");
112+
const [currentState, setCurrentState] = useState<StateKey>("idle");
41113
const [submissionId, setSubmissionId] = useState<string | null>(null);
114+
const [genericError, setGenericError] = useState("");
42115

43116
// auth + quota states
44117
const [me, setMe] = useState<Me>(null);
@@ -54,22 +127,15 @@ export default function RoleFitForm() {
54127

55128
const progressSteps = [
56129
"Upload CV",
57-
"Parse Job Description",
130+
"Parse Job Description",
58131
"Generate Report",
59132
"Redirect to Report",
60133
];
61134

62-
// Map backend statuses → step index
63-
const statusToStep: Record<string, number> = {
64-
submitted: 1,
65-
parsed_jd: 2,
66-
generated_report: 3,
67-
redirecting: 3,
68-
failed_parsing_jd: 1, // Show failure at Parse Job Description stage
69-
};
70-
71-
const currentStepIdx = statusToStep[submissionStatus] ?? 0;
72-
const percentDone = ((currentStepIdx + 1) / progressSteps.length) * 100;
135+
// Get current state configuration
136+
const stateConfig = STATE_CONFIG[currentState];
137+
const currentStepIdx = stateConfig.progressStep;
138+
const percentDone = currentStepIdx >= 0 ? ((currentStepIdx + 1) / progressSteps.length) * 100 : 0;
73139

74140
const DIRECTUS_URL = EXTERNAL.directus_url;
75141

@@ -251,20 +317,18 @@ export default function RoleFitForm() {
251317

252318
// Reset form state
253319
form.reset({ jobDescription: "", cv: null });
254-
setStep(0);
255-
setButtonText("Analyze Role Fit Now");
256320
setSubmissionId(null);
257-
setSubmissionStatus("redirecting");
321+
setCurrentState("redirecting");
258322

259323
// Redirect to report
260324
window.location.href = `/role-fit-index/report?id=${encodeURIComponent(js.data[0].id)}`;
261325
clearTimeout(timeout);
262326
resolve(true);
263327
return;
264328
}
265-
setError("Report ready but fetch failed.");
329+
setGenericError("Report ready but fetch failed.");
266330
} catch {
267-
setError("Report ready but fetch failed.");
331+
setGenericError("Report ready but fetch failed.");
268332
}
269333
};
270334

@@ -294,7 +358,9 @@ export default function RoleFitForm() {
294358
// Handle status updates
295359
if (rec.status) {
296360
console.log(rec.status);
297-
setSubmissionStatus(rec.status);
361+
if (rec.status in STATE_CONFIG) {
362+
setCurrentState(rec.status as StateKey);
363+
}
298364
}
299365

300366
if (rec.status === "generated_report") {
@@ -304,10 +370,11 @@ export default function RoleFitForm() {
304370
if ((rec.status || "").startsWith("failed_")) {
305371
if (rec.status === "failed_parsing_jd") {
306372
// Don't set a generic error - let the UI show the failure at the parsing step
373+
setCurrentState("failed_parsing_jd");
307374
clearTimeout(timeout);
308375
reject(new Error("Failed to parse job description"));
309376
} else {
310-
setError("Submission failed: " + rec.status);
377+
setGenericError("Submission failed: " + rec.status);
311378
clearTimeout(timeout);
312379
reject(new Error("Submission failed"));
313380
}
@@ -361,7 +428,6 @@ export default function RoleFitForm() {
361428

362429
const ws = new WebSocket(u.toString());
363430
wsRef.current = ws;
364-
setSubmissionStatus("submitted");
365431

366432
const timeout = setTimeout(() => {
367433
try { ws.close(); } catch { }
@@ -382,7 +448,7 @@ export default function RoleFitForm() {
382448

383449
ws.onclose = () => {
384450
clearTimeout(timeout);
385-
reject(new Error("WS closed"));
451+
reject(new Error("WS closed, something went wrong, try again!"));
386452
};
387453
} catch (e) {
388454
reject(e);
@@ -391,27 +457,21 @@ export default function RoleFitForm() {
391457
};
392458

393459
const onSubmit = async (values: FormValues) => {
394-
setSubmitting(true);
395460
try {
396-
setStep(1);
397-
setButtonText("Uploading CV…");
461+
setCurrentState("uploading");
398462
const cvFileId = await uploadFile(values.cv);
399463

400-
setButtonText("Saving submission…");
464+
setCurrentState("saving");
401465
const subId = await createSubmission(values.jobDescription, cvFileId);
402466
setSubmissionId(subId);
403467

404-
setStep(2);
405-
setButtonText("Analyzing…");
406-
setError("");
468+
setCurrentState("submitted");
469+
setGenericError("");
407470

408471
await subscribeWS(subId);
409472
} catch (err: any) {
410-
setError(err?.message || "Unexpected error");
411-
setStep(0);
412-
setButtonText("Analyze Role Fit Now");
413-
} finally {
414-
setSubmitting(false);
473+
setGenericError(err?.message || "Unexpected error");
474+
setCurrentState("idle");
415475
}
416476
};
417477

@@ -472,14 +532,14 @@ export default function RoleFitForm() {
472532
</div>
473533

474534
{/* Progress & Stepper */}
475-
{step >= 1 && (
535+
{stateConfig.step >= 1 && (
476536
<div className="mt-8 space-y-6">
477537
{/* Step circles */}
478538
<div className="flex justify-between">
479539
{progressSteps.map((s, i) => {
480540
const isCompleted = i < currentStepIdx;
481-
const isActive = i === currentStepIdx && !error && submissionStatus !== "failed_parsing_jd";
482-
const isFailed = (!!error && i === currentStepIdx) || (submissionStatus === "failed_parsing_jd" && i === 1);
541+
const isActive = i === currentStepIdx && !stateConfig.isError;
542+
const isFailed = stateConfig.isError && i === currentStepIdx;
483543

484544
return (
485545
<div key={s} className="flex flex-col items-center flex-1">
@@ -520,17 +580,16 @@ export default function RoleFitForm() {
520580

521581
{/* Helper text */}
522582
<p className="text-sm text-gray-600 text-center">
523-
{submissionStatus === "failed_parsing_jd"
524-
? "Failed to parse the job description. Please check the format and try again."
525-
: error
583+
{stateConfig.helperText ||
584+
(genericError
526585
? "Something went wrong. Please try again."
527-
: "Analyzing your CV & JD — this usually takes ~30 seconds. You'll be redirected when the report is ready."}
586+
: "Analyzing your CV & JD — this usually takes ~30 seconds. You'll be redirected when the report is ready.")}
528587
</p>
529588
</div>
530589
)}
531590

532591
{/* Error */}
533-
{error && <p className="text-sm text-red-600">{error}</p>}
592+
{genericError && <p className="text-sm text-red-600">{genericError}</p>}
534593

535594
{/* Credit display (RIGHT ABOVE THE BUTTON) */}
536595
<div className="text-center">
@@ -569,9 +628,9 @@ export default function RoleFitForm() {
569628
type="submit"
570629
variant="default"
571630
className="w-full"
572-
disabled={submitting || step !== 0}
631+
disabled={!stateConfig.canSubmit}
573632
>
574-
{buttonText}
633+
{stateConfig.buttonText}
575634
</Button>
576635
)}
577636

0 commit comments

Comments
 (0)