@@ -30,15 +30,88 @@ type FormValues = z.infer<typeof schema>;
3030
3131type 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+
33111export 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