CodeAntidote commited on
Commit
9f81f25
·
verified ·
1 Parent(s): 5573038

Add local JobAlign resume AI

Browse files
Files changed (1) hide show
  1. ai.js +681 -0
ai.js ADDED
@@ -0,0 +1,681 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import {
2
+ pipeline,
3
+ env
4
+ } from "https://cdn.jsdelivr.net/npm/@huggingface/transformers@3.8.1";
5
+
6
+
7
+ /* =========================================================
8
+ JOBALIGN LOCAL AI
9
+ Runs entirely in the user's browser.
10
+ ========================================================= */
11
+
12
+ env.allowLocalModels = false;
13
+
14
+ const MODEL_ID =
15
+ "onnx-community/Qwen2.5-0.5B-Instruct";
16
+
17
+
18
+ let generator = null;
19
+
20
+ let loadingPromise = null;
21
+
22
+
23
+ /* =========================================================
24
+ STATUS HELPER
25
+ ========================================================= */
26
+
27
+ function setAIStatus(message, type = "normal") {
28
+
29
+ const status =
30
+ document.getElementById(
31
+ "generationStatus"
32
+ );
33
+
34
+
35
+ if (!status) {
36
+ return;
37
+ }
38
+
39
+
40
+ let className = "";
41
+
42
+
43
+ if (type === "success") {
44
+
45
+ className =
46
+ "success-text";
47
+
48
+ } else if (type === "error") {
49
+
50
+ className =
51
+ "danger-text";
52
+
53
+ } else if (type === "warning") {
54
+
55
+ className =
56
+ "warning-text";
57
+ }
58
+
59
+
60
+ status.innerHTML = `
61
+
62
+ <div class="status-dot"></div>
63
+
64
+ <span class="${className}">
65
+ ${escapeHTML(message)}
66
+ </span>
67
+ `;
68
+ }
69
+
70
+
71
+ /* =========================================================
72
+ LOAD MODEL
73
+ ========================================================= */
74
+
75
+ async function loadModel() {
76
+
77
+ if (generator) {
78
+
79
+ return generator;
80
+ }
81
+
82
+
83
+ if (loadingPromise) {
84
+
85
+ return loadingPromise;
86
+ }
87
+
88
+
89
+ loadingPromise =
90
+ (async function () {
91
+
92
+ const hasWebGPU =
93
+ typeof navigator !==
94
+ "undefined" &&
95
+ "gpu" in navigator;
96
+
97
+
98
+ setAIStatus(
99
+ hasWebGPU
100
+ ? "Loading local AI model with WebGPU..."
101
+ : "Loading local AI model in CPU mode..."
102
+ );
103
+
104
+
105
+ const options = {
106
+
107
+ dtype: "q4",
108
+
109
+ progress_callback:
110
+ function (progress) {
111
+
112
+ updateProgress(
113
+ progress
114
+ );
115
+ }
116
+ };
117
+
118
+
119
+ if (hasWebGPU) {
120
+
121
+ options.device =
122
+ "webgpu";
123
+ }
124
+
125
+
126
+ try {
127
+
128
+ generator =
129
+ await pipeline(
130
+ "text-generation",
131
+ MODEL_ID,
132
+ options
133
+ );
134
+
135
+
136
+ setAIStatus(
137
+ "Local AI model ready",
138
+ "success"
139
+ );
140
+
141
+
142
+ return generator;
143
+
144
+
145
+ } catch (error) {
146
+
147
+ console.error(
148
+ "WebGPU/model load failed:",
149
+ error
150
+ );
151
+
152
+
153
+ /*
154
+ * If WebGPU failed,
155
+ * retry using browser CPU/WASM.
156
+ */
157
+
158
+ if (hasWebGPU) {
159
+
160
+ setAIStatus(
161
+ "WebGPU unavailable. Retrying with CPU..."
162
+ );
163
+
164
+
165
+ generator =
166
+ await pipeline(
167
+ "text-generation",
168
+ MODEL_ID,
169
+ {
170
+ dtype: "q4",
171
+
172
+ progress_callback:
173
+ function (progress) {
174
+
175
+ updateProgress(
176
+ progress
177
+ );
178
+ }
179
+ }
180
+ );
181
+
182
+
183
+ setAIStatus(
184
+ "Local AI model ready in CPU mode",
185
+ "success"
186
+ );
187
+
188
+
189
+ return generator;
190
+ }
191
+
192
+
193
+ throw error;
194
+ }
195
+
196
+ })();
197
+
198
+
199
+ try {
200
+
201
+ return await loadingPromise;
202
+
203
+ } catch (error) {
204
+
205
+ loadingPromise =
206
+ null;
207
+
208
+
209
+ setAIStatus(
210
+ "AI model could not load: " +
211
+ error.message,
212
+ "error"
213
+ );
214
+
215
+
216
+ throw error;
217
+ }
218
+ }
219
+
220
+
221
+ /* =========================================================
222
+ DOWNLOAD PROGRESS
223
+ ========================================================= */
224
+
225
+ function updateProgress(progress) {
226
+
227
+ if (!progress) {
228
+ return;
229
+ }
230
+
231
+
232
+ if (
233
+ progress.status ===
234
+ "progress" &&
235
+ typeof progress.progress ===
236
+ "number"
237
+ ) {
238
+
239
+ const percent =
240
+ Math.round(
241
+ progress.progress
242
+ );
243
+
244
+
245
+ setAIStatus(
246
+ "Downloading local AI model... " +
247
+ percent +
248
+ "%"
249
+ );
250
+
251
+
252
+ return;
253
+ }
254
+
255
+
256
+ if (
257
+ progress.status ===
258
+ "ready"
259
+ ) {
260
+
261
+ setAIStatus(
262
+ "AI model downloaded. Preparing..."
263
+ );
264
+ }
265
+ }
266
+
267
+
268
+ /* =========================================================
269
+ BUILD THE RESUME PROMPT
270
+ ========================================================= */
271
+
272
+ function buildResumeMessages({
273
+ resume,
274
+ jobDescription,
275
+ jobTitle,
276
+ company,
277
+ location
278
+ }) {
279
+
280
+ /*
281
+ * Keep inputs within a practical browser-model size.
282
+ */
283
+
284
+ const cleanResume =
285
+ truncateText(
286
+ resume,
287
+ 18000
288
+ );
289
+
290
+
291
+ const cleanJD =
292
+ truncateText(
293
+ jobDescription,
294
+ 14000
295
+ );
296
+
297
+
298
+ const systemPrompt = `
299
+ You are JobAlign, a technical resume alignment assistant.
300
+
301
+ Your job is to rewrite a candidate's existing resume for one specific target job.
302
+
303
+ CORE RULES:
304
+
305
+ 1. Preserve the candidate's employers, employment dates, education and factual career history.
306
+
307
+ 2. Strongly emphasize experience that matches the target job.
308
+
309
+ 3. Reorder and rewrite bullets so the target role becomes the natural interpretation of the candidate's background.
310
+
311
+ 4. Use important exact technical nouns from the job description naturally for ATS alignment.
312
+
313
+ 5. Do NOT copy sentences from the job description.
314
+
315
+ 6. Do NOT invent employers, projects, technologies, certifications, degrees, job titles, production systems or accomplishments that are unsupported by the master resume.
316
+
317
+ 7. Do NOT invent exact metrics. Preserve useful metrics already present in the resume.
318
+
319
+ 8. Use technically credible practitioner language. Avoid generic AI-style phrases such as:
320
+ results-driven,
321
+ proven track record,
322
+ leveraged,
323
+ dynamic professional,
324
+ passionate professional.
325
+
326
+ 9. Prefer concrete verbs such as:
327
+ Owned,
328
+ Built,
329
+ Designed,
330
+ Implemented,
331
+ Automated,
332
+ Migrated,
333
+ Tuned,
334
+ Optimized,
335
+ Diagnosed,
336
+ Integrated,
337
+ Delivered,
338
+ Reduced,
339
+ Resolved.
340
+
341
+ 10. Keep the resume ATS-safe:
342
+ plain text,
343
+ standard headings,
344
+ no tables,
345
+ no columns,
346
+ no icons,
347
+ no decorative symbols.
348
+
349
+ 11. Keep the summary concise.
350
+
351
+ 12. Put the most important job-specific technologies in context inside EXPERIENCE, not only SKILLS.
352
+
353
+ 13. Recent and highly relevant experience should receive the strongest detail.
354
+
355
+ 14. Keep projects only when they materially strengthen the target application.
356
+
357
+ 15. The final resume should sound like an experienced human practitioner, not like the job description was pasted into the resume.
358
+
359
+ OUTPUT ONLY THE COMPLETE REWRITTEN RESUME.
360
+
361
+ Use these headings where applicable:
362
+
363
+ SUMMARY
364
+ SKILLS
365
+ EXPERIENCE
366
+ PROJECTS
367
+ EDUCATION
368
+ CERTIFICATIONS
369
+
370
+ Do not include commentary before or after the resume.
371
+ `.trim();
372
+
373
+
374
+ const userPrompt = `
375
+ TARGET JOB
376
+
377
+ Title: ${jobTitle || "Not provided"}
378
+ Company: ${company || "Not provided"}
379
+ Location: ${location || "Not provided"}
380
+
381
+ TARGET JOB DESCRIPTION
382
+
383
+ ${cleanJD}
384
+
385
+
386
+ MASTER RESUME
387
+
388
+ ${cleanResume}
389
+
390
+
391
+ Rewrite the complete master resume for this specific position.
392
+
393
+ Prioritize recruiter relevance, ATS keyword coverage, technical credibility and natural human wording.
394
+
395
+ Return only the finished copy-paste-ready resume.
396
+ `.trim();
397
+
398
+
399
+ return [
400
+
401
+ {
402
+ role: "system",
403
+ content:
404
+ systemPrompt
405
+ },
406
+
407
+ {
408
+ role: "user",
409
+ content:
410
+ userPrompt
411
+ }
412
+
413
+ ];
414
+ }
415
+
416
+
417
+ /* =========================================================
418
+ GENERATE RESUME
419
+ ========================================================= */
420
+
421
+ async function generateResume({
422
+ resume,
423
+ jobDescription,
424
+ jobTitle,
425
+ company,
426
+ location
427
+ }) {
428
+
429
+ if (
430
+ !resume ||
431
+ resume.trim().length < 100
432
+ ) {
433
+
434
+ throw new Error(
435
+ "Master resume is missing."
436
+ );
437
+ }
438
+
439
+
440
+ if (
441
+ !jobDescription ||
442
+ jobDescription
443
+ .trim()
444
+ .length < 100
445
+ ) {
446
+
447
+ throw new Error(
448
+ "Job description is missing."
449
+ );
450
+ }
451
+
452
+
453
+ const model =
454
+ await loadModel();
455
+
456
+
457
+ setAIStatus(
458
+ "Rewriting resume locally..."
459
+ );
460
+
461
+
462
+ const messages =
463
+ buildResumeMessages({
464
+ resume,
465
+ jobDescription,
466
+ jobTitle,
467
+ company,
468
+ location
469
+ });
470
+
471
+
472
+ const output =
473
+ await model(
474
+ messages,
475
+ {
476
+ max_new_tokens:
477
+ 1800,
478
+
479
+ do_sample:
480
+ false,
481
+
482
+ repetition_penalty:
483
+ 1.08
484
+ }
485
+ );
486
+
487
+
488
+ let generatedText = "";
489
+
490
+
491
+ try {
492
+
493
+ const generated =
494
+ output[0]
495
+ .generated_text;
496
+
497
+
498
+ if (
499
+ Array.isArray(
500
+ generated
501
+ )
502
+ ) {
503
+
504
+ const lastMessage =
505
+ generated[
506
+ generated.length -
507
+ 1
508
+ ];
509
+
510
+
511
+ generatedText =
512
+ lastMessage.content ||
513
+ "";
514
+
515
+ } else {
516
+
517
+ generatedText =
518
+ String(
519
+ generated || ""
520
+ );
521
+ }
522
+
523
+
524
+ } catch (error) {
525
+
526
+ console.error(
527
+ "Could not parse model output:",
528
+ error
529
+ );
530
+
531
+
532
+ throw new Error(
533
+ "The AI returned an unreadable response."
534
+ );
535
+ }
536
+
537
+
538
+ generatedText =
539
+ cleanGeneratedResume(
540
+ generatedText
541
+ );
542
+
543
+
544
+ if (
545
+ generatedText.length <
546
+ 200
547
+ ) {
548
+
549
+ throw new Error(
550
+ "The AI response was too short. Please try again."
551
+ );
552
+ }
553
+
554
+
555
+ setAIStatus(
556
+ "Targeted resume generated",
557
+ "success"
558
+ );
559
+
560
+
561
+ return generatedText;
562
+ }
563
+
564
+
565
+ /* =========================================================
566
+ CLEAN OUTPUT
567
+ ========================================================= */
568
+
569
+ function cleanGeneratedResume(text) {
570
+
571
+ let result =
572
+ String(
573
+ text || ""
574
+ )
575
+ .trim();
576
+
577
+
578
+ result =
579
+ result.replace(
580
+ /^```(?:text|markdown)?/i,
581
+ ""
582
+ );
583
+
584
+
585
+ result =
586
+ result.replace(
587
+ /```$/i,
588
+ ""
589
+ );
590
+
591
+
592
+ result =
593
+ result.replace(
594
+ /^Here(?:'s| is).*?resume[:\s]*/i,
595
+ ""
596
+ );
597
+
598
+
599
+ return result.trim();
600
+ }
601
+
602
+
603
+ /* =========================================================
604
+ TRUNCATE SAFELY
605
+ ========================================================= */
606
+
607
+ function truncateText(
608
+ text,
609
+ maxCharacters
610
+ ) {
611
+
612
+ const value =
613
+ String(
614
+ text || ""
615
+ )
616
+ .trim();
617
+
618
+
619
+ if (
620
+ value.length <=
621
+ maxCharacters
622
+ ) {
623
+
624
+ return value;
625
+ }
626
+
627
+
628
+ return (
629
+ value.slice(
630
+ 0,
631
+ maxCharacters
632
+ ) +
633
+ "\n\n[Input shortened for local browser processing.]"
634
+ );
635
+ }
636
+
637
+
638
+ /* =========================================================
639
+ HTML SAFETY
640
+ ========================================================= */
641
+
642
+ function escapeHTML(value) {
643
+
644
+ const element =
645
+ document.createElement(
646
+ "div"
647
+ );
648
+
649
+
650
+ element.textContent =
651
+ String(
652
+ value || ""
653
+ );
654
+
655
+
656
+ return element.innerHTML;
657
+ }
658
+
659
+
660
+ /* =========================================================
661
+ EXPOSE AI TO APP.JS
662
+ ========================================================= */
663
+
664
+ window.JobAlignAI = {
665
+
666
+ generateResume:
667
+ generateResume,
668
+
669
+ loadModel:
670
+ loadModel,
671
+
672
+ model:
673
+ MODEL_ID
674
+ };
675
+
676
+
677
+ window.dispatchEvent(
678
+ new CustomEvent(
679
+ "jobalign-ai-ready"
680
+ )
681
+ );