CodeAntidote commited on
Commit
7340c6b
·
verified ·
1 Parent(s): 594e5e6

Replace local model with Cloudflare resume AI

Browse files
Files changed (1) hide show
  1. ai.js +94 -466
ai.js CHANGED
@@ -1,35 +1,19 @@
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
 
10
  A product by John_D
11
 
12
- IMPORTANT:
13
- - CPU/WASM only
14
- - NO WebGPU
15
- - Smaller instruction model
16
- ========================================================= */
17
-
18
-
19
- env.allowLocalModels = false;
20
-
21
 
22
- /* =========================================================
23
- MODEL
 
24
  ========================================================= */
25
 
26
- const MODEL_ID =
27
- "onnx-community/SmolLM2-360M-Instruct-ONNX";
28
-
29
-
30
- let generator = null;
31
 
32
- let loadingPromise = null;
 
33
 
34
 
35
  /* =========================================================
@@ -83,277 +67,6 @@ function setAIStatus(
83
  }
84
 
85
 
86
- /* =========================================================
87
- LOAD MODEL
88
- ========================================================= */
89
-
90
- async function loadModel() {
91
-
92
- if (generator) {
93
-
94
- return generator;
95
- }
96
-
97
-
98
- if (loadingPromise) {
99
-
100
- return loadingPromise;
101
- }
102
-
103
-
104
- loadingPromise =
105
- (async function () {
106
-
107
- setAIStatus(
108
- "Loading lightweight local AI model..."
109
- );
110
-
111
-
112
- try {
113
-
114
- generator =
115
- await pipeline(
116
- "text-generation",
117
- MODEL_ID,
118
- {
119
- /*
120
- * Force browser CPU.
121
- *
122
- * This avoids the WebGPU crash
123
- * shown in Chrome Console.
124
- */
125
-
126
- device:
127
- "wasm",
128
-
129
- /*
130
- * 4-bit model keeps download
131
- * and memory requirements lower.
132
- */
133
-
134
- dtype:
135
- "q4",
136
-
137
- progress_callback:
138
- function (progress) {
139
-
140
- updateProgress(
141
- progress
142
- );
143
- }
144
- }
145
- );
146
-
147
-
148
- setAIStatus(
149
- "Local AI model ready",
150
- "success"
151
- );
152
-
153
-
154
- return generator;
155
-
156
-
157
- } catch (error) {
158
-
159
- console.error(
160
- "JobAlign model load error:",
161
- error
162
- );
163
-
164
-
165
- loadingPromise =
166
- null;
167
-
168
-
169
- setAIStatus(
170
- "AI model failed to load: " +
171
- error.message,
172
- "error"
173
- );
174
-
175
-
176
- throw error;
177
- }
178
-
179
- })();
180
-
181
-
182
- return loadingPromise;
183
- }
184
-
185
-
186
- /* =========================================================
187
- MODEL DOWNLOAD STATUS
188
- ========================================================= */
189
-
190
- function updateProgress(
191
- progress
192
- ) {
193
-
194
- if (!progress) {
195
-
196
- return;
197
- }
198
-
199
-
200
- if (
201
- progress.status ===
202
- "progress" &&
203
- typeof progress.progress ===
204
- "number"
205
- ) {
206
-
207
- const percent =
208
- Math.round(
209
- progress.progress
210
- );
211
-
212
-
213
- setAIStatus(
214
- "Downloading lightweight AI model... " +
215
- percent +
216
- "%"
217
- );
218
-
219
-
220
- return;
221
- }
222
-
223
-
224
- if (
225
- progress.status ===
226
- "ready"
227
- ) {
228
-
229
- setAIStatus(
230
- "Model downloaded. Starting AI..."
231
- );
232
- }
233
- }
234
-
235
-
236
- /* =========================================================
237
- BUILD PROMPT
238
- ========================================================= */
239
-
240
- function buildResumeMessages({
241
- resume,
242
- jobDescription,
243
- jobTitle,
244
- company,
245
- location
246
- }) {
247
-
248
- /*
249
- * Browser models have smaller context windows
250
- * than ChatGPT.
251
-
252
- * Keep the most useful amount of resume/JD text
253
- * while avoiding memory problems.
254
- */
255
-
256
- const cleanResume =
257
- truncateText(
258
- resume,
259
- 11000
260
- );
261
-
262
-
263
- const cleanJD =
264
- truncateText(
265
- jobDescription,
266
- 7000
267
- );
268
-
269
-
270
- const systemPrompt = `
271
- You are JobAlign, a technical resume alignment assistant.
272
-
273
- Rewrite an existing resume for one specific target job.
274
-
275
- RULES:
276
-
277
- - Preserve the candidate's real employers, dates, education and career history.
278
- - Do not invent employers, certifications, degrees or projects.
279
- - Do not invent exact metrics.
280
- - Keep useful metrics already present in the resume.
281
- - Emphasize experience relevant to the target job.
282
- - Reorder bullets so the target role is obvious to a recruiter.
283
- - Use important technical nouns from the job description naturally.
284
- - Do not copy job-description sentences.
285
- - Use practical technical language.
286
- - Avoid phrases such as results-driven, proven track record, leveraged and passionate professional.
287
- - Prefer verbs such as Built, Designed, Implemented, Automated, Migrated, Tuned, Optimized, Diagnosed, Integrated, Delivered, Reduced and Resolved.
288
- - Keep the output ATS-safe.
289
- - Use plain text only.
290
- - Do not use tables or columns.
291
- - Keep the summary concise.
292
- - Put important technologies inside EXPERIENCE as well as SKILLS.
293
- - Recent and relevant experience should receive the most detail.
294
- - Preserve projects only when they strengthen this target role.
295
-
296
- OUTPUT ONLY THE FINISHED RESUME.
297
-
298
- Use these headings when applicable:
299
-
300
- SUMMARY
301
- SKILLS
302
- EXPERIENCE
303
- PROJECTS
304
- EDUCATION
305
- CERTIFICATIONS
306
- `.trim();
307
-
308
-
309
- const userPrompt = `
310
- TARGET JOB
311
-
312
- Title: ${jobTitle || "Not provided"}
313
- Company: ${company || "Not provided"}
314
- Location: ${location || "Not provided"}
315
-
316
-
317
- JOB DESCRIPTION
318
-
319
- ${cleanJD}
320
-
321
-
322
- MASTER RESUME
323
-
324
- ${cleanResume}
325
-
326
-
327
- Rewrite this resume specifically for the target job.
328
-
329
- Make it recruiter-friendly, ATS-friendly and technically credible.
330
-
331
- Return only the completed resume.
332
- `.trim();
333
-
334
-
335
- return [
336
-
337
- {
338
- role:
339
- "system",
340
-
341
- content:
342
- systemPrompt
343
- },
344
-
345
- {
346
- role:
347
- "user",
348
-
349
- content:
350
- userPrompt
351
- }
352
-
353
- ];
354
- }
355
-
356
-
357
  /* =========================================================
358
  GENERATE RESUME
359
  ========================================================= */
@@ -368,8 +81,7 @@ async function generateResume({
368
 
369
  if (
370
  !resume ||
371
- resume.trim().length <
372
- 100
373
  ) {
374
 
375
  throw new Error(
@@ -380,10 +92,7 @@ async function generateResume({
380
 
381
  if (
382
  !jobDescription ||
383
- jobDescription
384
- .trim()
385
- .length <
386
- 100
387
  ) {
388
 
389
  throw new Error(
@@ -392,146 +101,104 @@ async function generateResume({
392
  }
393
 
394
 
395
- const model =
396
- await loadModel();
397
-
398
-
399
  setAIStatus(
400
- "Analyzing job and rewriting resume..."
401
  );
402
 
403
 
404
- const messages =
405
- buildResumeMessages({
406
- resume:
407
- resume,
 
 
408
 
409
- jobDescription:
410
- jobDescription,
 
411
 
412
- jobTitle:
413
- jobTitle,
 
414
 
415
- company:
416
- company,
 
 
417
 
418
- location:
419
- location
420
- });
421
 
 
 
422
 
423
- let output;
 
424
 
 
 
 
 
 
425
 
426
- try {
427
-
428
- output =
429
- await model(
430
- messages,
431
- {
432
- /*
433
- * Keep generation practical
434
- * for laptop CPU inference.
435
- */
436
 
437
- max_new_tokens:
438
- 1100,
439
 
440
- do_sample:
441
- false,
442
 
443
- repetition_penalty:
444
- 1.08
445
- }
446
- );
447
 
 
 
448
 
449
  } catch (error) {
450
 
451
- console.error(
452
- "JobAlign generation error:",
453
- error
454
- );
455
-
456
-
457
  throw new Error(
458
- "Local resume generation failed: " +
459
- error.message
460
  );
461
  }
462
 
463
 
464
- let generatedText =
465
- "";
466
 
 
 
 
 
467
 
468
- try {
469
 
470
- const result =
471
- output[0]
472
- .generated_text;
473
-
474
-
475
- /*
476
- * Chat-generation pipelines often
477
- * return an array of messages.
478
- */
479
-
480
- if (
481
- Array.isArray(
482
- result
483
- )
484
- ) {
485
-
486
- const finalMessage =
487
- result[
488
- result.length -
489
- 1
490
- ];
491
-
492
-
493
- generatedText =
494
- finalMessage.content ||
495
- "";
496
-
497
-
498
- } else {
499
-
500
- generatedText =
501
- String(
502
- result ||
503
- ""
504
- );
505
- }
506
-
507
-
508
- } catch (error) {
509
-
510
- console.error(
511
- "JobAlign output parsing error:",
512
- error
513
  );
 
 
514
 
 
 
 
 
515
 
516
  throw new Error(
517
- "The AI returned an unreadable response."
518
  );
519
  }
520
 
521
 
522
- generatedText =
523
- cleanGeneratedResume(
524
- generatedText
525
- );
526
 
527
 
528
  if (
529
- generatedText.length <
530
- 300
531
  ) {
532
 
533
  throw new Error(
534
- "The generated resume was too short. Please try again."
535
  );
536
  }
537
 
@@ -542,84 +209,46 @@ async function generateResume({
542
  );
543
 
544
 
545
- return generatedText;
546
  }
547
 
548
 
549
  /* =========================================================
550
- OUTPUT CLEANUP
551
  ========================================================= */
552
 
553
- function cleanGeneratedResume(
554
- text
555
- ) {
556
-
557
- let result =
558
- String(
559
- text ||
560
- ""
561
- )
562
- .trim();
563
 
 
564
 
565
- result =
566
- result.replace(
567
- /^```(?:text|markdown)?/i,
568
- ""
569
- );
570
 
571
 
572
- result =
573
- result.replace(
574
- /```$/i,
575
- ""
576
- );
577
 
578
 
579
- result =
580
- result.replace(
581
- /^Here(?:'s| is).*?resume[:\s]*/i,
582
- ""
583
  );
584
 
585
 
586
- return result.trim();
587
- }
588
-
589
-
590
- /* =========================================================
591
- INPUT SIZE CONTROL
592
- ========================================================= */
593
-
594
- function truncateText(
595
- text,
596
- maxCharacters
597
- ) {
598
 
599
- const value =
600
- String(
601
- text ||
602
- ""
603
- )
604
- .trim();
605
 
606
 
607
- if (
608
- value.length <=
609
- maxCharacters
610
- ) {
611
-
612
- return value;
613
  }
614
-
615
-
616
- return (
617
- value.substring(
618
- 0,
619
- maxCharacters
620
- ) +
621
- "\n\n[Input shortened for local browser processing.]"
622
- );
623
  }
624
 
625
 
@@ -639,8 +268,7 @@ function escapeHTML(
639
 
640
  element.textContent =
641
  String(
642
- value ||
643
- ""
644
  );
645
 
646
 
@@ -649,7 +277,7 @@ function escapeHTML(
649
 
650
 
651
  /* =========================================================
652
- EXPOSE AI TO APP.JS
653
  ========================================================= */
654
 
655
  window.JobAlignAI = {
@@ -657,11 +285,11 @@ window.JobAlignAI = {
657
  generateResume:
658
  generateResume,
659
 
660
- loadModel:
661
- loadModel,
662
 
663
- model:
664
- MODEL_ID
665
  };
666
 
667
 
 
 
 
 
 
 
 
1
  /* =========================================================
2
+ JOBALIGN AI CLIENT
3
 
4
  A product by John_D
5
 
6
+ Resume generation now runs through the
7
+ Cloudflare Worker backend.
 
 
 
 
 
 
 
8
 
9
+ No local LLM download.
10
+ No WebGPU.
11
+ No WASM model.
12
  ========================================================= */
13
 
 
 
 
 
 
14
 
15
+ const JOBALIGN_BACKEND =
16
+ "https://jobalign-adzuna-proxy.darbymjohn.workers.dev";
17
 
18
 
19
  /* =========================================================
 
67
  }
68
 
69
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
70
  /* =========================================================
71
  GENERATE RESUME
72
  ========================================================= */
 
81
 
82
  if (
83
  !resume ||
84
+ resume.trim().length < 100
 
85
  ) {
86
 
87
  throw new Error(
 
92
 
93
  if (
94
  !jobDescription ||
95
+ jobDescription.trim().length < 100
 
 
 
96
  ) {
97
 
98
  throw new Error(
 
101
  }
102
 
103
 
 
 
 
 
104
  setAIStatus(
105
+ "Analyzing job and rebuilding resume..."
106
  );
107
 
108
 
109
+ const response =
110
+ await fetch(
111
+ JOBALIGN_BACKEND + "/resume",
112
+ {
113
+ method:
114
+ "POST",
115
 
116
+ headers: {
117
+ "Content-Type":
118
+ "application/json",
119
 
120
+ "Accept":
121
+ "application/json"
122
+ },
123
 
124
+ body:
125
+ JSON.stringify({
126
+ resume:
127
+ resume,
128
 
129
+ jobDescription:
130
+ jobDescription,
 
131
 
132
+ jobTitle:
133
+ jobTitle,
134
 
135
+ company:
136
+ company,
137
 
138
+ location:
139
+ location
140
+ })
141
+ }
142
+ );
143
 
 
 
 
 
 
 
 
 
 
 
144
 
145
+ let data;
 
146
 
 
 
147
 
148
+ try {
 
 
 
149
 
150
+ data =
151
+ await response.json();
152
 
153
  } catch (error) {
154
 
 
 
 
 
 
 
155
  throw new Error(
156
+ "Resume service returned an unreadable response."
 
157
  );
158
  }
159
 
160
 
161
+ if (!response.ok) {
 
162
 
163
+ const details =
164
+ data.details
165
+ ? " " + data.details
166
+ : "";
167
 
 
168
 
169
+ throw new Error(
170
+ (
171
+ data.error ||
172
+ "Resume generation failed."
173
+ ) +
174
+ details
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
175
  );
176
+ }
177
+
178
 
179
+ if (
180
+ !data.ok ||
181
+ !data.resume
182
+ ) {
183
 
184
  throw new Error(
185
+ "Resume service did not return a completed resume."
186
  );
187
  }
188
 
189
 
190
+ const generatedResume =
191
+ String(
192
+ data.resume
193
+ ).trim();
194
 
195
 
196
  if (
197
+ generatedResume.length < 300
 
198
  ) {
199
 
200
  throw new Error(
201
+ "The generated resume was incomplete."
202
  );
203
  }
204
 
 
209
  );
210
 
211
 
212
+ return generatedResume;
213
  }
214
 
215
 
216
  /* =========================================================
217
+ OPTIONAL HEALTH CHECK
218
  ========================================================= */
219
 
220
+ async function checkBackend() {
 
 
 
 
 
 
 
 
 
221
 
222
+ try {
223
 
224
+ const response =
225
+ await fetch(
226
+ JOBALIGN_BACKEND +
227
+ "/health"
228
+ );
229
 
230
 
231
+ const data =
232
+ await response.json();
 
 
 
233
 
234
 
235
+ return Boolean(
236
+ response.ok &&
237
+ data.ok &&
238
+ data.aiConfigured
239
  );
240
 
241
 
242
+ } catch (error) {
 
 
 
 
 
 
 
 
 
 
 
243
 
244
+ console.error(
245
+ "JobAlign backend health check failed:",
246
+ error
247
+ );
 
 
248
 
249
 
250
+ return false;
 
 
 
 
 
251
  }
 
 
 
 
 
 
 
 
 
252
  }
253
 
254
 
 
268
 
269
  element.textContent =
270
  String(
271
+ value || ""
 
272
  );
273
 
274
 
 
277
 
278
 
279
  /* =========================================================
280
+ EXPOSE TO APP.JS
281
  ========================================================= */
282
 
283
  window.JobAlignAI = {
 
285
  generateResume:
286
  generateResume,
287
 
288
+ checkBackend:
289
+ checkBackend,
290
 
291
+ mode:
292
+ "cloudflare-workers-ai"
293
  };
294
 
295