/* ai.jsx — Groq API integration (server-proxied) */ // Clean up any previously stored key from localStorage (function() { try { localStorage.removeItem("bdr_groq_key"); } catch(e) {} })(); var GROQ_MODEL = "llama-3.3-70b-versatile"; // Free-tier AI allowance (must match FREE_LIMIT in landing /api/ai/chat). window.AI_FREE_LIMIT = 10; // Notify the UI that the remaining free-use count changed. function _notifyAIUses() { try { window.dispatchEvent(new CustomEvent("bdr-ai-uses-changed")); } catch (e) {} } // Pro is granted via ?plan=pro in the URL OR a previously persisted plan. // Keep both in sync so AI UI and export logic agree across reloads. window.IS_PRO = (function() { try { if (new URLSearchParams(location.search).get("plan") === "pro") { localStorage.setItem("bdr_plan", "pro"); return true; } return localStorage.getItem("bdr_plan") === "pro"; } catch (e) { return new URLSearchParams(location.search).get("plan") === "pro"; } })(); window.GROQ_SYSTEM = "You are an expert résumé writer and career coach with 15+ years of experience. You write tight, impactful résumé content that:\n- Uses strong action verbs (Led, Built, Launched, Grew, Reduced, Shipped, Automated, Negotiated, etc.)\n- Quantifies achievements with specific metrics wherever honestly possible\n- Is concise, ATS-friendly, and avoids clichés and buzzwords\n- Focuses on business impact and results, not just duties\n\nFormatting rules you must always follow:\n- Never use em dashes (—); use a regular hyphen (-) instead\n- Never use the word \"spearheaded\"; use \"managed\" instead\n\nRespond with ONLY the requested content - no preamble, explanations, or meta-commentary unless explicitly asked. Be direct and professional."; window.buildResumeCtx = function(data) { var b = data.basics || {}; var lines = []; if (b.name) lines.push("Name: " + b.name); if (b.title) lines.push("Title: " + b.title); if (b.location) lines.push("Location: " + b.location); if (data.summary) lines.push("\nProfessional Summary:\n" + data.summary); if ((data.experience || []).length) { lines.push("\nWork Experience:"); (data.experience || []).forEach(function(x) { var dates = x.start ? (x.start + "–" + (x.end || "Present")) : (x.dates || ""); lines.push(" " + (x.role || "") + " at " + (x.company || "") + (dates ? " (" + dates + ")" : "")); (x.bullets || []).filter(Boolean).forEach(function(bl) { lines.push(" • " + bl); }); }); } if ((data.projects || []).length) { lines.push("\nProjects:"); (data.projects || []).forEach(function(p) { lines.push(" " + (p.name || "") + (p.tech ? " [" + p.tech + "]" : "")); (p.bullets || []).filter(Boolean).forEach(function(bl) { lines.push(" • " + bl); }); }); } var allSkills = (data.skills || []).flatMap ? (data.skills || []).flatMap(function(g) { return g.items || []; }) : []; if (allSkills.length) lines.push("\nSkills: " + allSkills.join(", ")); (data.education || []).forEach(function(e) { lines.push("\nEducation: " + (e.degree || "") + " — " + (e.school || "")); }); if ((data.certs || []).length) lines.push("\nCertifications: " + (data.certs || []).join(", ")); return lines.join("\n"); }; async function _streamGroqResponse(res, onChunk, onDone, onError) { // Track remaining uses from response header var remaining = res.headers.get("X-AI-Uses-Remaining"); if (remaining !== null && remaining !== "unlimited") { try { localStorage.setItem("bdr_ai_uses_remaining", remaining); } catch(e) {} _notifyAIUses(); } else if (remaining === "unlimited") { try { localStorage.setItem("bdr_ai_uses_remaining", "unlimited"); } catch(e) {} _notifyAIUses(); } var reader = res.body.getReader(); var dec = new TextDecoder(); var buf = ""; var full = ""; while (true) { var chunk = await reader.read(); if (chunk.done) break; buf += dec.decode(chunk.value, { stream: true }); var lines = buf.split("\n"); buf = lines.pop(); for (var i = 0; i < lines.length; i++) { var line = lines[i]; if (!line.startsWith("data: ")) continue; var raw = line.slice(6).trim(); if (raw === "[DONE]") { onDone(full); return; } try { var parsed = JSON.parse(raw); var delta = (parsed.choices && parsed.choices[0] && parsed.choices[0].delta && parsed.choices[0].delta.content) || ""; if (delta) { full += delta; onChunk(delta, full); } } catch(e3) {} } } onDone(full); } window.groqStream = async function(opts) { var messages = opts.messages || []; var model = opts.model || GROQ_MODEL; var maxTokens = opts.maxTokens || 1024; var onChunk = opts.onChunk || function() {}; var onDone = opts.onDone || function() {}; var onError = opts.onError || function() {}; try { var landingUrl = (window.__LANDING_URL__ || "http://localhost:3401"); var res = await fetch(landingUrl + "/api/ai/chat", { method: "POST", headers: { "Content-Type": "application/json" }, credentials: "include", body: JSON.stringify({ model: model, messages: messages, max_tokens: maxTokens }), }); if (res.status === 429) { // Out of free uses — pin the cached count to 0 so the meter reflects it. try { localStorage.setItem("bdr_ai_uses_remaining", "0"); } catch(e) {} _notifyAIUses(); onError("free_limit_reached"); return; } if (!res.ok) { var errMsg = res.statusText; try { var j = await res.json(); errMsg = (j.error && j.error.message) || j.error || errMsg; } catch(e2) {} onError("AI error: " + errMsg); return; } await _streamGroqResponse(res, onChunk, onDone, onError); } catch(e) { onError(e.message || "Network error — check your internet connection."); } }; // Shared upgrade card shown when free AI limit is hit function ProUpgradeCard({ onClose }) { const LANDING = window.__LANDING_URL__ || "http://localhost:3401"; return (

You've used all free AI credits

Upgrade to Pro for unlimited access to every AI feature — interview prep, salary insights, achievement rewrites, translations, and more.

Upgrade to Pro →
); } // Expose remaining uses (from localStorage cache, updated after each call) window.getAIUsesRemaining = function() { try { var v = localStorage.getItem("bdr_ai_uses_remaining"); if (v === "unlimited") return Infinity; if (v !== null) return parseInt(v, 10); } catch(e) {} return null; // unknown — will be set after first call };