CodeAntidote commited on
Commit
0784d09
·
verified ·
1 Parent(s): 9dd6a61

Fix JobAlign job search workflow

Browse files
Files changed (1) hide show
  1. app.js +361 -606
app.js CHANGED
@@ -1,21 +1,23 @@
1
- const resumeFile = document.getElementById("resumeFile");
2
- const resumeText = document.getElementById("resumeText");
3
- const resumeStatus = document.getElementById("resumeStatus");
4
-
5
  const jobKeyword = document.getElementById("jobKeyword");
6
  const jobLocation = document.getElementById("jobLocation");
7
- const jobSite = document.getElementById("jobSite");
8
-
9
- const searchJobsBtn = document.getElementById("searchJobsBtn");
10
 
 
 
 
 
11
  const jobUrl = document.getElementById("jobUrl");
12
- const openJobBtn = document.getElementById("openJobBtn");
13
- const pasteJobBtn = document.getElementById("pasteJobBtn");
14
 
15
- const jobDescription = document.getElementById("jobDescription");
16
 
17
- const analyzeBtn = document.getElementById("analyzeBtn");
18
- const clearBtn = document.getElementById("clearBtn");
 
 
 
 
19
 
20
  const atsScore = document.getElementById("atsScore");
21
  const keywordScore = document.getElementById("keywordScore");
@@ -24,184 +26,247 @@ const missingSkills = document.getElementById("missingSkills");
24
 
25
  const analysisResults = document.getElementById("analysisResults");
26
 
27
- const generateResumeBtn =
28
- document.getElementById("generateResumeBtn");
 
29
 
30
- const copyResumeBtn =
31
- document.getElementById("copyResumeBtn");
 
32
 
33
- const downloadResumeBtn =
34
- document.getElementById("downloadResumeBtn");
35
 
36
- const resumeOutput =
37
- document.getElementById("resumeOutput");
 
38
 
39
- const interviewCheck =
40
- document.getElementById("interviewCheck");
41
 
 
42
 
43
- /* ----------------------------------------------------
44
- RESUME INPUT
45
- ---------------------------------------------------- */
46
 
47
- resumeText.addEventListener("input", () => {
48
-
49
- if (resumeText.value.trim().length > 50) {
50
 
51
  resumeStatus.innerHTML = `
52
  <div class="status-dot"></div>
53
  <span class="success-text">
54
- Reference resume loaded
55
- </span>
56
- `;
57
-
58
- } else {
59
-
60
- resumeStatus.innerHTML = `
61
- <div class="status-dot"></div>
62
- <span>
63
- Waiting for reference resume
64
  </span>
65
  `;
66
  }
67
  });
68
 
69
 
70
- resumeFile.addEventListener("change", async () => {
 
 
71
 
72
- const file = resumeFile.files[0];
 
 
 
 
 
 
 
 
 
 
73
 
74
- if (!file) {
75
  return;
76
  }
77
 
78
- const extension =
79
- file.name.split(".").pop().toLowerCase();
80
-
81
- if (extension === "txt") {
82
 
83
- const text = await file.text();
84
 
85
- resumeText.value = text;
86
 
87
- resumeStatus.innerHTML = `
88
- <div class="status-dot"></div>
89
- <span class="success-text">
90
- ${escapeHtml(file.name)} loaded
91
- </span>
92
- `;
93
 
94
- } else {
 
95
 
96
- resumeStatus.innerHTML = `
97
- <div class="status-dot"></div>
98
- <span class="warning-text">
99
- ${escapeHtml(file.name)} selected.
100
- PDF/DOCX reading will be added next.
101
- For now, paste the resume text below.
102
- </span>
103
- `;
104
- }
105
- });
106
 
 
 
107
 
108
- /* ----------------------------------------------------
109
- JOB SEARCH
110
- ---------------------------------------------------- */
111
 
112
- searchJobsBtn.addEventListener("click", () => {
113
 
114
- const keyword =
115
- jobKeyword.value.trim();
116
 
117
- const location =
118
- jobLocation.value.trim();
119
 
120
- if (!keyword) {
 
 
 
 
 
 
 
121
 
122
- alert("Enter a job keyword first.");
123
 
124
- jobKeyword.focus();
125
 
126
- return;
 
 
 
 
 
 
127
  }
128
 
129
- const query =
130
- encodeURIComponent(keyword);
131
 
132
- const place =
133
- encodeURIComponent(location);
 
 
 
 
 
 
 
 
 
134
 
135
- let searchUrl = "";
136
 
137
- switch (jobSite.value) {
 
 
 
 
 
 
 
138
 
139
- case "linkedin":
140
 
141
- searchUrl =
142
- `https://www.linkedin.com/jobs/search/?keywords=${query}&location=${place}`;
143
 
144
- break;
145
 
 
 
 
146
 
147
- case "indeed":
 
 
 
 
 
 
148
 
149
- searchUrl =
150
- `https://www.indeed.com/jobs?q=${query}&l=${place}`;
151
 
152
- break;
 
 
 
 
153
 
 
154
 
155
- case "google":
156
 
157
- searchUrl =
158
- `https://www.google.com/search?q=${query}+jobs+${place}`;
 
 
 
 
 
 
 
 
 
159
 
160
- break;
161
 
 
162
 
163
- case "glassdoor":
164
 
165
- searchUrl =
166
- `https://www.google.com/search?q=site%3Aglassdoor.com%2Fjob-listing+${query}+${place}`;
 
 
 
 
167
 
168
- break;
 
 
 
 
 
 
169
 
170
 
171
- case "company":
 
 
 
 
 
 
 
172
 
173
- searchUrl =
174
- `https://www.google.com/search?q=${query}+${place}+company+careers`;
175
 
176
- break;
 
 
 
 
 
 
177
 
 
 
 
178
 
179
- default:
180
 
181
- searchUrl =
182
- `https://www.google.com/search?q=${query}+jobs+${place}`;
183
- }
184
 
185
  window.open(
186
- searchUrl,
187
  "_blank",
188
  "noopener,noreferrer"
189
  );
190
- });
191
 
192
 
193
- /* ----------------------------------------------------
194
- JOB URL
195
- ---------------------------------------------------- */
196
 
197
- openJobBtn.addEventListener("click", () => {
198
 
199
- let url =
200
- jobUrl.value.trim();
 
 
 
 
 
 
 
 
 
 
 
 
 
201
 
202
  if (!url) {
203
 
204
- alert("Paste a job URL first.");
205
 
206
  return;
207
  }
@@ -211,8 +276,7 @@ openJobBtn.addEventListener("click", () => {
211
  !url.startsWith("http://")
212
  ) {
213
 
214
- url =
215
- "https://" + url;
216
  }
217
 
218
  window.open(
@@ -223,43 +287,81 @@ openJobBtn.addEventListener("click", () => {
223
  });
224
 
225
 
226
- /* ----------------------------------------------------
227
- CLIPBOARD
228
- ---------------------------------------------------- */
229
 
230
- pasteJobBtn.addEventListener("click", async () => {
231
 
232
- try {
233
 
234
- const text =
235
- await navigator.clipboard.readText();
236
 
237
- if (!text) {
238
 
239
- alert(
240
- "Your clipboard is empty."
241
- );
242
 
243
- return;
244
- }
 
 
245
 
246
- jobDescription.value =
247
- text;
 
 
 
 
 
248
 
249
- } catch (error) {
250
 
251
- alert(
252
- "Your browser blocked clipboard access. Paste the job description manually using Command + V."
253
- );
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
254
  }
255
  });
256
 
257
 
258
- /* ----------------------------------------------------
259
- ATS KEYWORDS
260
- ---------------------------------------------------- */
261
 
262
- const technicalSkills = [
263
 
264
  "python",
265
  "sql",
@@ -268,44 +370,31 @@ const technicalSkills = [
268
  "typescript",
269
  "c#",
270
  ".net",
271
- ".net core",
272
- "asp.net",
273
  "angular",
274
  "react",
275
- "node.js",
276
 
277
  "postgresql",
278
  "mysql",
279
- "sql server",
280
- "oracle",
281
  "mongodb",
282
  "cassandra",
283
  "couchbase",
284
  "redis",
285
- "dynamodb",
286
 
287
  "aws",
288
  "azure",
289
  "gcp",
290
  "rds",
291
  "aurora",
292
- "ec2",
293
  "s3",
294
- "lambda",
295
- "glue",
296
- "athena",
297
 
298
  "terraform",
299
  "ansible",
300
  "docker",
301
  "kubernetes",
302
- "jenkins",
303
- "github actions",
304
- "ci/cd",
305
 
306
  "linux",
307
  "bash",
308
- "powershell",
309
 
310
  "spark",
311
  "pyspark",
@@ -316,25 +405,10 @@ const technicalSkills = [
316
 
317
  "kafka",
318
  "kinesis",
319
- "rabbitmq",
320
- "sqs",
321
 
322
  "etl",
323
  "elt",
324
  "cdc",
325
- "data modeling",
326
- "data warehouse",
327
- "data lake",
328
-
329
- "rest api",
330
- "rest",
331
- "microservices",
332
- "distributed systems",
333
-
334
- "grafana",
335
- "prometheus",
336
- "opentelemetry",
337
- "splunk",
338
 
339
  "machine learning",
340
  "pytorch",
@@ -342,433 +416,262 @@ const technicalSkills = [
342
  "scikit-learn",
343
  "mlflow",
344
 
345
- "llm",
346
- "generative ai",
347
- "genai",
348
- "rag",
349
- "transformers",
350
-
351
  "power bi",
352
  "tableau",
353
 
354
  "jira",
355
  "confluence",
356
 
357
- "high availability",
358
- "disaster recovery",
359
- "backup",
360
- "recovery",
361
- "replication",
362
 
363
- "performance tuning",
364
- "query optimization",
365
- "indexing",
366
 
367
  "agile",
368
- "scrum"
369
- ];
370
 
 
 
 
 
 
 
 
 
 
371
 
372
- /* ----------------------------------------------------
373
- ANALYZE
374
- ---------------------------------------------------- */
375
 
376
- analyzeBtn.addEventListener("click", () => {
377
 
378
  const resume =
379
- normalizeText(
380
- resumeText.value
381
- );
382
 
383
  const jd =
384
- normalizeText(
385
- jobDescription.value
386
- );
387
 
388
- if (resume.length < 100) {
389
 
390
- alert(
391
- "Add your reference resume first."
392
- );
393
 
394
- resumeText.focus();
395
 
396
  return;
397
  }
398
 
399
- if (jd.length < 100) {
400
 
401
- alert(
402
- "Paste the complete job description first."
403
- );
404
 
405
- jobDescription.focus();
406
 
407
  return;
408
  }
409
 
410
 
411
- const jdSkills =
412
- technicalSkills.filter(
413
- skill =>
414
- containsSkill(jd, skill)
415
  );
416
 
417
 
418
- const resumeSkills =
419
- jdSkills.filter(
420
- skill =>
421
- containsSkill(resume, skill)
422
  );
423
 
424
 
425
  const gaps =
426
- jdSkills.filter(
427
- skill =>
428
- !containsSkill(resume, skill)
429
  );
430
 
431
 
432
  const coverage =
433
- jdSkills.length
434
- ? Math.round(
435
- (
436
- resumeSkills.length /
437
- jdSkills.length
438
- ) * 100
439
- )
440
- : 50;
441
-
442
-
443
- const roleWords =
444
- getImportantWords(jd);
445
-
446
-
447
- const resumeWords =
448
- new Set(
449
- normalizeText(resume)
450
- .split(/\s+/)
451
- );
452
-
453
-
454
- const overlappingWords =
455
- roleWords.filter(
456
- word =>
457
- resumeWords.has(word)
458
- );
459
-
460
-
461
- const semanticCoverage =
462
- roleWords.length
463
  ? Math.round(
464
- (
465
- overlappingWords.length /
466
- roleWords.length
467
- ) * 100
468
  )
469
  : 50;
470
 
471
 
472
- const calculatedAts =
473
  Math.min(
474
- 98,
475
  Math.max(
476
- 20,
477
- Math.round(
478
- coverage * 0.7 +
479
- semanticCoverage * 0.3
480
- )
481
  )
482
  );
483
 
484
 
485
  atsScore.textContent =
486
- calculatedAts + "%";
487
-
488
 
489
  keywordScore.textContent =
490
  coverage + "%";
491
 
492
-
493
  matchedSkills.textContent =
494
- resumeSkills.length;
495
-
496
 
497
  missingSkills.textContent =
498
  gaps.length;
499
 
500
 
501
- const matchedDisplay =
502
- resumeSkills.length
503
- ? resumeSkills
504
- .map(capitalizeSkill)
505
- .join(", ")
506
- : "No high-value technical keywords detected yet.";
507
-
508
-
509
- const gapDisplay =
510
- gaps.length
511
- ? gaps
512
- .map(capitalizeSkill)
513
- .join(", ")
514
- : "No obvious technical keyword gaps detected.";
515
-
516
-
517
  analysisResults.innerHTML = `
518
 
519
- <div style="margin-bottom:18px;">
520
 
521
  <strong>
522
  Strong matches
523
  </strong>
524
 
525
  <p style="
526
- color:#bcbcbc;
527
  margin-top:7px;
 
528
  ">
529
- ${escapeHtml(matchedDisplay)}
 
 
 
 
530
  </p>
531
 
532
  </div>
533
 
534
 
535
- <div>
536
 
537
  <strong>
538
- Skills emphasized in the JD but not found in your resume
539
  </strong>
540
 
541
  <p style="
542
- color:#bcbcbc;
543
  margin-top:7px;
 
544
  ">
545
- ${escapeHtml(gapDisplay)}
 
 
 
 
546
  </p>
547
 
548
  </div>
549
-
550
-
551
- <div style="
552
- margin-top:20px;
553
- padding-top:18px;
554
- border-top:1px solid #262626;
555
- color:#777;
556
- font-size:12px;
557
- ">
558
-
559
- This is a local keyword/ATS comparison.
560
- It does not invent missing experience.
561
-
562
- </div>
563
- `;
564
-
565
-
566
- window.jobAlignAnalysis = {
567
-
568
- atsScore:
569
- calculatedAts,
570
-
571
- coverage:
572
- coverage,
573
-
574
- matched:
575
- resumeSkills,
576
-
577
- gaps:
578
- gaps
579
-
580
- };
581
- });
582
-
583
-
584
- /* ----------------------------------------------------
585
- CLEAR JOB
586
- ---------------------------------------------------- */
587
-
588
- clearBtn.addEventListener("click", () => {
589
-
590
- jobUrl.value = "";
591
-
592
- jobDescription.value = "";
593
-
594
- atsScore.textContent = "--";
595
- keywordScore.textContent = "--";
596
- matchedSkills.textContent = "--";
597
- missingSkills.textContent = "--";
598
-
599
- analysisResults.innerHTML = `
600
-
601
- <div class="empty-state">
602
-
603
- Add your resume and job description,
604
- then click
605
-
606
- <strong>
607
- Analyze Match
608
- </strong>.
609
-
610
- </div>
611
- `;
612
-
613
- resumeOutput.textContent =
614
- "Your tailored resume will appear here.";
615
-
616
- interviewCheck.innerHTML = `
617
-
618
- <div class="empty-state">
619
-
620
- Interview-defense questions will appear
621
- after the resume is generated.
622
-
623
- </div>
624
  `;
625
  });
626
 
627
 
628
- /* ----------------------------------------------------
629
- GENERATE BUTTON
630
- ---------------------------------------------------- */
631
 
632
  generateResumeBtn.addEventListener("click", () => {
633
 
634
- const resume =
635
- resumeText.value.trim();
636
 
637
- const jd =
638
- jobDescription.value.trim();
639
 
640
- if (resume.length < 100) {
641
 
642
  alert(
643
- "Add your reference resume first."
644
  );
645
 
646
  return;
647
  }
648
 
649
- if (jd.length < 100) {
 
650
 
651
  alert(
652
- "Paste the target job description first."
653
  );
654
 
655
  return;
656
  }
657
 
658
 
659
- resumeOutput.textContent = `AI RESUME ENGINE — READY FOR CONNECTION
660
-
661
- Your reference resume and target job have been captured successfully.
662
-
663
- Current app capabilities:
664
- • Job-site keyword search
665
- • Job URL opening
666
- • Job-description capture
667
- • Local ATS comparison
668
- • Keyword matching
669
- • Gap identification
670
- • Resume copy/download
671
 
672
- NEXT CONNECTION:
673
- The free AI rewrite engine will be connected in the next build step.
674
 
675
- The AI engine will receive:
 
676
 
677
- REFERENCE RESUME
678
- ----------------
679
- ${resume}
680
 
681
- TARGET JOB DESCRIPTION
682
- ----------------------
683
- ${jd}
684
- `;
685
 
 
686
 
687
- interviewCheck.innerHTML = `
688
 
689
- <strong>
690
- Resume engine input validated.
691
- </strong>
692
 
693
- <p style="
694
- color:#aaa;
695
- margin-top:10px;
696
- ">
 
 
 
697
 
698
- The next step connects the hard-realignment
699
- instructions so the app can produce the actual
700
- targeted resume and interviewer-defense section.
701
-
702
- </p>
703
- `;
704
  });
705
 
706
 
707
- /* ----------------------------------------------------
708
  COPY
709
- ---------------------------------------------------- */
710
 
711
  copyResumeBtn.addEventListener("click", async () => {
712
 
713
  const text =
714
  resumeOutput.innerText.trim();
715
 
716
- if (
717
- !text ||
718
- text ===
719
- "Your tailored resume will appear here."
720
- ) {
721
 
722
- alert(
723
- "Generate a resume first."
724
- );
725
 
726
  return;
727
  }
728
 
729
- try {
730
 
731
- await navigator.clipboard.writeText(
732
- text
733
- );
734
-
735
- copyResumeBtn.textContent =
736
- "Copied";
737
 
738
- setTimeout(() => {
739
 
740
- copyResumeBtn.textContent =
741
- "Copy Resume";
742
-
743
- }, 1500);
744
-
745
- } catch (error) {
746
 
747
- alert(
748
- "Copy failed. Select the text manually."
749
- );
750
- }
751
  });
752
 
753
 
754
- /* ----------------------------------------------------
755
  DOWNLOAD
756
- ---------------------------------------------------- */
757
 
758
  downloadResumeBtn.addEventListener("click", () => {
759
 
760
  const text =
761
  resumeOutput.innerText.trim();
762
 
763
- if (
764
- !text ||
765
- text ===
766
- "Your tailored resume will appear here."
767
- ) {
768
 
769
- alert(
770
- "Generate a resume first."
771
- );
772
 
773
  return;
774
  }
@@ -778,8 +681,7 @@ downloadResumeBtn.addEventListener("click", () => {
778
  new Blob(
779
  [text],
780
  {
781
- type:
782
- "text/plain;charset=utf-8"
783
  }
784
  );
785
 
@@ -792,177 +694,22 @@ downloadResumeBtn.addEventListener("click", () => {
792
  document.createElement("a");
793
 
794
 
795
- link.href =
796
- url;
797
 
798
  link.download =
799
- "JobAlign_Tailored_Resume.txt";
800
-
801
 
802
- document.body.appendChild(link);
803
 
804
  link.click();
805
 
806
- document.body.removeChild(link);
807
 
808
  URL.revokeObjectURL(url);
809
  });
810
 
811
 
812
- /* ----------------------------------------------------
813
  HELPERS
814
- ---------------------------------------------------- */
815
-
816
- function normalizeText(text) {
817
-
818
- return text
819
- .toLowerCase()
820
- .replace(/[^\w+#./-]+/g, " ")
821
- .replace(/\s+/g, " ")
822
- .trim();
823
- }
824
-
825
-
826
- function containsSkill(text, skill) {
827
-
828
- const normalizedSkill =
829
- normalizeText(skill);
830
-
831
- return text.includes(
832
- normalizedSkill
833
- );
834
- }
835
-
836
-
837
- function capitalizeSkill(skill) {
838
-
839
- const exceptions = {
840
-
841
- "aws": "AWS",
842
- "gcp": "GCP",
843
- "sql": "SQL",
844
- "etl": "ETL",
845
- "elt": "ELT",
846
- "cdc": "CDC",
847
- "llm": "LLM",
848
- "rag": "RAG",
849
- "genai": "GenAI",
850
- "ci/cd": "CI/CD",
851
- "ec2": "EC2",
852
- "s3": "S3",
853
- "sqs": "SQS",
854
- "rds": "RDS",
855
- "dbt": "dbt",
856
- "c#": "C#",
857
- ".net": ".NET",
858
- ".net core": ".NET Core",
859
- "asp.net": "ASP.NET"
860
- };
861
-
862
-
863
- if (exceptions[skill]) {
864
-
865
- return exceptions[skill];
866
- }
867
-
868
-
869
- return skill
870
- .split(" ")
871
- .map(
872
- word =>
873
- word.charAt(0).toUpperCase() +
874
- word.slice(1)
875
- )
876
- .join(" ");
877
- }
878
-
879
-
880
- function getImportantWords(text) {
881
-
882
- const stopWords =
883
- new Set([
884
-
885
- "the",
886
- "and",
887
- "for",
888
- "with",
889
- "that",
890
- "this",
891
- "from",
892
- "will",
893
- "you",
894
- "your",
895
- "our",
896
- "are",
897
- "have",
898
- "has",
899
- "into",
900
- "using",
901
- "use",
902
- "who",
903
- "their",
904
- "they",
905
- "job",
906
- "role",
907
- "team",
908
- "work",
909
- "working",
910
- "skills",
911
- "experience",
912
- "years",
913
- "including",
914
- "such",
915
- "other",
916
- "about",
917
- "within",
918
- "across",
919
- "required",
920
- "preferred",
921
- "responsibilities",
922
- "responsibility",
923
- "qualification",
924
- "qualifications",
925
- "candidate",
926
- "position"
927
-
928
- ]);
929
-
930
-
931
- const words =
932
- normalizeText(text)
933
- .split(/\s+/)
934
- .filter(
935
- word =>
936
- word.length > 4 &&
937
- !stopWords.has(word)
938
- );
939
-
940
-
941
- const frequency = {};
942
-
943
-
944
- words.forEach(word => {
945
-
946
- frequency[word] =
947
- (frequency[word] || 0) + 1;
948
- });
949
-
950
-
951
- return Object.entries(frequency)
952
-
953
- .sort(
954
- (a, b) =>
955
- b[1] - a[1]
956
- )
957
-
958
- .slice(0, 40)
959
-
960
- .map(
961
- entry =>
962
- entry[0]
963
- );
964
- }
965
-
966
 
967
  function escapeHtml(text) {
968
 
@@ -973,4 +720,12 @@ function escapeHtml(text) {
973
  text;
974
 
975
  return div.innerHTML;
 
 
 
 
 
 
 
 
976
  }
 
1
+ const searchJobsBtn = document.getElementById("searchJobsBtn");
 
 
 
2
  const jobKeyword = document.getElementById("jobKeyword");
3
  const jobLocation = document.getElementById("jobLocation");
4
+ const jobSource = document.getElementById("jobSource");
5
+ const jobResults = document.getElementById("jobResults");
 
6
 
7
+ const selectedJobTitle = document.getElementById("selectedJobTitle");
8
+ const selectedCompany = document.getElementById("selectedCompany");
9
+ const selectedLocation = document.getElementById("selectedLocation");
10
+ const jobDescription = document.getElementById("jobDescription");
11
  const jobUrl = document.getElementById("jobUrl");
 
 
12
 
13
+ const openOriginalJobBtn = document.getElementById("openOriginalJobBtn");
14
 
15
+ const resumeFile = document.getElementById("resumeFile");
16
+ const resumeText = document.getElementById("resumeText");
17
+ const saveResumeBtn = document.getElementById("saveResumeBtn");
18
+ const resumeStatus = document.getElementById("resumeStatus");
19
+
20
+ const analyzeJobBtn = document.getElementById("analyzeJobBtn");
21
 
22
  const atsScore = document.getElementById("atsScore");
23
  const keywordScore = document.getElementById("keywordScore");
 
26
 
27
  const analysisResults = document.getElementById("analysisResults");
28
 
29
+ const generateResumeBtn = document.getElementById("generateResumeBtn");
30
+ const copyResumeBtn = document.getElementById("copyResumeBtn");
31
+ const downloadResumeBtn = document.getElementById("downloadResumeBtn");
32
 
33
+ const generationStatus = document.getElementById("generationStatus");
34
+ const resumeOutput = document.getElementById("resumeOutput");
35
+ const interviewCheck = document.getElementById("interviewCheck");
36
 
 
 
37
 
38
+ /* ======================================================
39
+ LOAD SAVED MASTER RESUME
40
+ ====================================================== */
41
 
42
+ window.addEventListener("load", () => {
 
43
 
44
+ const savedResume = localStorage.getItem("jobalign_master_resume");
45
 
46
+ if (savedResume) {
 
 
47
 
48
+ resumeText.value = savedResume;
 
 
49
 
50
  resumeStatus.innerHTML = `
51
  <div class="status-dot"></div>
52
  <span class="success-text">
53
+ Master resume loaded from this browser
 
 
 
 
 
 
 
 
 
54
  </span>
55
  `;
56
  }
57
  });
58
 
59
 
60
+ /* ======================================================
61
+ JOB SEARCH
62
+ ====================================================== */
63
 
64
+ searchJobsBtn.addEventListener("click", () => {
65
+
66
+ const keyword = jobKeyword.value.trim();
67
+ const location = jobLocation.value.trim();
68
+ const source = jobSource.value;
69
+
70
+ if (!keyword) {
71
+
72
+ alert("Enter a job title or keyword first.");
73
+
74
+ jobKeyword.focus();
75
 
 
76
  return;
77
  }
78
 
79
+ showSearchOptions(keyword, location, source);
80
+ });
 
 
81
 
 
82
 
83
+ function showSearchOptions(keyword, location, source) {
84
 
85
+ const encodedKeyword = encodeURIComponent(keyword);
86
+ const encodedLocation = encodeURIComponent(location);
 
 
 
 
87
 
88
+ const linkedinUrl =
89
+ `https://www.linkedin.com/jobs/search/?keywords=${encodedKeyword}&location=${encodedLocation}`;
90
 
91
+ const indeedUrl =
92
+ `https://www.indeed.com/jobs?q=${encodedKeyword}&l=${encodedLocation}`;
 
 
 
 
 
 
 
 
93
 
94
+ const googleUrl =
95
+ `https://www.google.com/search?q=${encodedKeyword}+jobs+${encodedLocation}`;
96
 
97
+ const companyUrl =
98
+ `https://www.google.com/search?q=${encodedKeyword}+${encodedLocation}+careers`;
 
99
 
100
+ let cards = "";
101
 
 
 
102
 
103
+ if (source === "all" || source === "linkedin") {
 
104
 
105
+ cards += createSearchCard(
106
+ "LinkedIn Jobs",
107
+ keyword,
108
+ location,
109
+ linkedinUrl,
110
+ "linkedin"
111
+ );
112
+ }
113
 
 
114
 
115
+ if (source === "all" || source === "indeed") {
116
 
117
+ cards += createSearchCard(
118
+ "Indeed",
119
+ keyword,
120
+ location,
121
+ indeedUrl,
122
+ "indeed"
123
+ );
124
  }
125
 
 
 
126
 
127
+ if (source === "all") {
128
+
129
+ cards += createSearchCard(
130
+ "Google Jobs Search",
131
+ keyword,
132
+ location,
133
+ googleUrl,
134
+ "google"
135
+ );
136
+ }
137
+
138
 
139
+ if (source === "all" || source === "company") {
140
 
141
+ cards += createSearchCard(
142
+ "Company Career Pages",
143
+ keyword,
144
+ location,
145
+ companyUrl,
146
+ "company"
147
+ );
148
+ }
149
 
 
150
 
151
+ jobResults.innerHTML = `
 
152
 
153
+ <div style="margin-bottom:18px;">
154
 
155
+ <strong style="font-size:17px;">
156
+ Search ready for:
157
+ </strong>
158
 
159
+ <div style="
160
+ color:#aaa;
161
+ margin-top:6px;
162
+ ">
163
+ ${escapeHtml(keyword)}
164
+ ${location ? " • " + escapeHtml(location) : ""}
165
+ </div>
166
 
167
+ </div>
 
168
 
169
+ <div style="
170
+ display:grid;
171
+ grid-template-columns:repeat(auto-fit,minmax(240px,1fr));
172
+ gap:12px;
173
+ ">
174
 
175
+ ${cards}
176
 
177
+ </div>
178
 
179
+ <div style="
180
+ margin-top:18px;
181
+ color:#777;
182
+ font-size:12px;
183
+ ">
184
+ Open a job source, choose the position you want,
185
+ then copy its job URL and description back into JobAlign.
186
+ The browser helper we add later will automate that capture.
187
+ </div>
188
+ `;
189
+ }
190
 
 
191
 
192
+ function createSearchCard(name, keyword, location, url, source) {
193
 
194
+ return `
195
 
196
+ <div style="
197
+ border:1px solid #2c2c2c;
198
+ border-radius:14px;
199
+ padding:18px;
200
+ background:#080808;
201
+ ">
202
 
203
+ <div style="
204
+ font-weight:700;
205
+ font-size:16px;
206
+ margin-bottom:7px;
207
+ ">
208
+ ${escapeHtml(name)}
209
+ </div>
210
 
211
 
212
+ <div style="
213
+ color:#888;
214
+ font-size:13px;
215
+ min-height:42px;
216
+ ">
217
+ Search ${escapeHtml(keyword)}
218
+ ${location ? " in " + escapeHtml(location) : ""}
219
+ </div>
220
 
 
 
221
 
222
+ <button
223
+ class="secondary-btn"
224
+ style="margin-top:14px;"
225
+ onclick="openJobSearch('${escapeAttribute(url)}')"
226
+ >
227
+ Open Search
228
+ </button>
229
 
230
+ </div>
231
+ `;
232
+ }
233
 
 
234
 
235
+ window.openJobSearch = function(url) {
 
 
236
 
237
  window.open(
238
+ url,
239
  "_blank",
240
  "noopener,noreferrer"
241
  );
242
+ };
243
 
244
 
245
+ /* ======================================================
246
+ SELECT JOB MANUALLY
247
+ ====================================================== */
248
 
249
+ jobUrl.addEventListener("input", () => {
250
 
251
+ if (jobUrl.value.trim()) {
252
+
253
+ generationStatus.innerHTML = `
254
+ <div class="status-dot"></div>
255
+ <span>
256
+ Job URL captured
257
+ </span>
258
+ `;
259
+ }
260
+ });
261
+
262
+
263
+ openOriginalJobBtn.addEventListener("click", () => {
264
+
265
+ let url = jobUrl.value.trim();
266
 
267
  if (!url) {
268
 
269
+ alert("Paste the original job URL first.");
270
 
271
  return;
272
  }
 
276
  !url.startsWith("http://")
277
  ) {
278
 
279
+ url = "https://" + url;
 
280
  }
281
 
282
  window.open(
 
287
  });
288
 
289
 
290
+ /* ======================================================
291
+ SAVE MASTER RESUME
292
+ ====================================================== */
293
 
294
+ saveResumeBtn.addEventListener("click", () => {
295
 
296
+ const resume = resumeText.value.trim();
297
 
298
+ if (resume.length < 100) {
 
299
 
300
+ alert("Paste your full master resume first.");
301
 
302
+ return;
303
+ }
 
304
 
305
+ localStorage.setItem(
306
+ "jobalign_master_resume",
307
+ resume
308
+ );
309
 
310
+ resumeStatus.innerHTML = `
311
+ <div class="status-dot"></div>
312
+ <span class="success-text">
313
+ Master resume saved in this browser
314
+ </span>
315
+ `;
316
+ });
317
 
 
318
 
319
+ /* ======================================================
320
+ TXT FILE SUPPORT
321
+ ====================================================== */
322
+
323
+ resumeFile.addEventListener("change", async () => {
324
+
325
+ const file = resumeFile.files[0];
326
+
327
+ if (!file) {
328
+ return;
329
+ }
330
+
331
+ const extension =
332
+ file.name.split(".").pop().toLowerCase();
333
+
334
+ if (extension === "txt") {
335
+
336
+ const text = await file.text();
337
+
338
+ resumeText.value = text;
339
+
340
+ resumeStatus.innerHTML = `
341
+ <div class="status-dot"></div>
342
+ <span class="success-text">
343
+ Resume loaded
344
+ </span>
345
+ `;
346
+
347
+ } else {
348
+
349
+ resumeStatus.innerHTML = `
350
+ <div class="status-dot"></div>
351
+ <span class="warning-text">
352
+ PDF/DOCX selected. Paste the resume text for now.
353
+ Direct document reading comes in a later step.
354
+ </span>
355
+ `;
356
  }
357
  });
358
 
359
 
360
+ /* ======================================================
361
+ BASIC MATCH ANALYSIS
362
+ ====================================================== */
363
 
364
+ const skills = [
365
 
366
  "python",
367
  "sql",
 
370
  "typescript",
371
  "c#",
372
  ".net",
 
 
373
  "angular",
374
  "react",
 
375
 
376
  "postgresql",
377
  "mysql",
 
 
378
  "mongodb",
379
  "cassandra",
380
  "couchbase",
381
  "redis",
 
382
 
383
  "aws",
384
  "azure",
385
  "gcp",
386
  "rds",
387
  "aurora",
 
388
  "s3",
389
+ "ec2",
 
 
390
 
391
  "terraform",
392
  "ansible",
393
  "docker",
394
  "kubernetes",
 
 
 
395
 
396
  "linux",
397
  "bash",
 
398
 
399
  "spark",
400
  "pyspark",
 
405
 
406
  "kafka",
407
  "kinesis",
 
 
408
 
409
  "etl",
410
  "elt",
411
  "cdc",
 
 
 
 
 
 
 
 
 
 
 
 
 
412
 
413
  "machine learning",
414
  "pytorch",
 
416
  "scikit-learn",
417
  "mlflow",
418
 
 
 
 
 
 
 
419
  "power bi",
420
  "tableau",
421
 
422
  "jira",
423
  "confluence",
424
 
425
+ "product management",
426
+ "product manager",
427
+ "roadmap",
428
+ "stakeholder",
429
+ "backlog",
430
 
431
+ "program management",
432
+ "technical program management",
 
433
 
434
  "agile",
435
+ "scrum",
 
436
 
437
+ "rest",
438
+ "api",
439
+ "microservices",
440
+
441
+ "high availability",
442
+ "disaster recovery",
443
+ "replication",
444
+ "performance tuning"
445
+ ];
446
 
 
 
 
447
 
448
+ analyzeJobBtn.addEventListener("click", () => {
449
 
450
  const resume =
451
+ resumeText.value.toLowerCase();
 
 
452
 
453
  const jd =
454
+ jobDescription.value.toLowerCase();
 
 
455
 
 
456
 
457
+ if (resume.length < 100) {
 
 
458
 
459
+ alert("Save or paste your master resume first.");
460
 
461
  return;
462
  }
463
 
 
464
 
465
+ if (jd.length < 100) {
 
 
466
 
467
+ alert("Paste the selected job description first.");
468
 
469
  return;
470
  }
471
 
472
 
473
+ const requiredSkills =
474
+ skills.filter(
475
+ skill => jd.includes(skill)
 
476
  );
477
 
478
 
479
+ const matched =
480
+ requiredSkills.filter(
481
+ skill => resume.includes(skill)
 
482
  );
483
 
484
 
485
  const gaps =
486
+ requiredSkills.filter(
487
+ skill => !resume.includes(skill)
 
488
  );
489
 
490
 
491
  const coverage =
492
+ requiredSkills.length
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
493
  ? Math.round(
494
+ matched.length /
495
+ requiredSkills.length *
496
+ 100
 
497
  )
498
  : 50;
499
 
500
 
501
+ const ats =
502
  Math.min(
503
+ 96,
504
  Math.max(
505
+ 25,
506
+ coverage
 
 
 
507
  )
508
  );
509
 
510
 
511
  atsScore.textContent =
512
+ ats + "%";
 
513
 
514
  keywordScore.textContent =
515
  coverage + "%";
516
 
 
517
  matchedSkills.textContent =
518
+ matched.length;
 
519
 
520
  missingSkills.textContent =
521
  gaps.length;
522
 
523
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
524
  analysisResults.innerHTML = `
525
 
526
+ <div>
527
 
528
  <strong>
529
  Strong matches
530
  </strong>
531
 
532
  <p style="
533
+ color:#aaa;
534
  margin-top:7px;
535
+ line-height:1.7;
536
  ">
537
+ ${
538
+ matched.length
539
+ ? escapeHtml(matched.join(", "))
540
+ : "No major technical matches detected yet."
541
+ }
542
  </p>
543
 
544
  </div>
545
 
546
 
547
+ <div style="margin-top:18px;">
548
 
549
  <strong>
550
+ JD priorities not found in master resume
551
  </strong>
552
 
553
  <p style="
554
+ color:#aaa;
555
  margin-top:7px;
556
+ line-height:1.7;
557
  ">
558
+ ${
559
+ gaps.length
560
+ ? escapeHtml(gaps.join(", "))
561
+ : "No obvious keyword gaps found."
562
+ }
563
  </p>
564
 
565
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
566
  `;
567
  });
568
 
569
 
570
+ /* ======================================================
571
+ GENERATE PLACEHOLDER
572
+ ====================================================== */
573
 
574
  generateResumeBtn.addEventListener("click", () => {
575
 
576
+ const resume = resumeText.value.trim();
577
+ const jd = jobDescription.value.trim();
578
 
 
 
579
 
580
+ if (jd.length < 100) {
581
 
582
  alert(
583
+ "First select a job and paste/capture its job description."
584
  );
585
 
586
  return;
587
  }
588
 
589
+
590
+ if (resume.length < 100) {
591
 
592
  alert(
593
+ "Add your master resume before generating."
594
  );
595
 
596
  return;
597
  }
598
 
599
 
600
+ generationStatus.innerHTML = `
601
+ <div class="status-dot"></div>
602
+ <span class="success-text">
603
+ Job and master resume are ready
604
+ </span>
605
+ `;
 
 
 
 
 
 
606
 
 
 
607
 
608
+ resumeOutput.textContent =
609
+ `JOBALIGN AI ENGINE READY
610
 
611
+ Selected Position:
612
+ ${selectedJobTitle.value || "Target job"}
 
613
 
614
+ Company:
615
+ ${selectedCompany.value || "Target employer"}
 
 
616
 
617
+ The job description and your master resume have both been captured.
618
 
619
+ The next build step connects the hard-realignment AI engine.
620
 
621
+ The final workflow will be:
 
 
622
 
623
+ SEARCH JOB
624
+ → CHOOSE JOB
625
+ → READ JOB DESCRIPTION
626
+ → RESEARCH COMPANY
627
+ → HARD-REALIGN MASTER RESUME
628
+ → GENERATE TARGETED RESUME
629
+ → DOWNLOAD`;
630
 
 
 
 
 
 
 
631
  });
632
 
633
 
634
+ /* ======================================================
635
  COPY
636
+ ====================================================== */
637
 
638
  copyResumeBtn.addEventListener("click", async () => {
639
 
640
  const text =
641
  resumeOutput.innerText.trim();
642
 
643
+ if (!text) {
 
 
 
 
644
 
645
+ alert("Generate a resume first.");
 
 
646
 
647
  return;
648
  }
649
 
650
+ await navigator.clipboard.writeText(text);
651
 
652
+ copyResumeBtn.textContent = "Copied";
 
 
 
 
 
653
 
654
+ setTimeout(() => {
655
 
656
+ copyResumeBtn.textContent =
657
+ "Copy Resume";
 
 
 
 
658
 
659
+ }, 1500);
 
 
 
660
  });
661
 
662
 
663
+ /* ======================================================
664
  DOWNLOAD
665
+ ====================================================== */
666
 
667
  downloadResumeBtn.addEventListener("click", () => {
668
 
669
  const text =
670
  resumeOutput.innerText.trim();
671
 
672
+ if (!text) {
 
 
 
 
673
 
674
+ alert("Generate a resume first.");
 
 
675
 
676
  return;
677
  }
 
681
  new Blob(
682
  [text],
683
  {
684
+ type: "text/plain;charset=utf-8"
 
685
  }
686
  );
687
 
 
694
  document.createElement("a");
695
 
696
 
697
+ link.href = url;
 
698
 
699
  link.download =
700
+ "JobAlign_Resume.txt";
 
701
 
 
702
 
703
  link.click();
704
 
 
705
 
706
  URL.revokeObjectURL(url);
707
  });
708
 
709
 
710
+ /* ======================================================
711
  HELPERS
712
+ ====================================================== */
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
713
 
714
  function escapeHtml(text) {
715
 
 
720
  text;
721
 
722
  return div.innerHTML;
723
+ }
724
+
725
+
726
+ function escapeAttribute(text) {
727
+
728
+ return String(text)
729
+ .replace(/'/g, "\\'")
730
+ .replace(/"/g, "&quot;");
731
  }