File size: 9,605 Bytes
62e5b72 79701df f61335f 62e5b72 f61335f 62e5b72 907631d 62e5b72 907631d 62e5b72 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 | <!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Joblin - Settings</title>
<link rel="stylesheet" href="styles.css">
</head>
<body>
<div class="app-layout">
<aside class="sidebar" id="sidebar">
<a href="dashboard.html" class="sidebar-brand">
<div class="brand-icon">J</div>
<h1>Job<span>lin</span></h1>
</a>
<nav class="sidebar-nav">
<div class="sidebar-section-title">Main</div>
<a href="dashboard.html" class="nav-item">
<span class="nav-icon">■</span>
<span class="nav-label">Dashboard</span>
</a>
<a href="jobs.html" class="nav-item">
<span class="nav-icon">▶</span>
<span class="nav-label">All Jobs</span>
</a>
<a href="manual-job.html" class="nav-item">
<span class="nav-icon">+</span>
<span class="nav-label">Manual Entry</span>
</a>
<div class="sidebar-section-title">My Profile</div>
<a href="make-cv.html" class="nav-item">
<span class="nav-icon">✍</span>
<span class="nav-label">Make My CV</span>
</a>
<a href="cv-editor.html" class="nav-item">
<span class="nav-icon">✎</span>
<span class="nav-label">Edit CV</span>
</a>
<a href="settings.html" class="nav-item active">
<span class="nav-icon">⚙</span>
<span class="nav-label">Settings</span>
</a>
<a href="about.html" class="nav-item">
<span class="nav-icon">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><line x1="12" y1="16" x2="12" y2="12"/><line x1="12" y1="8" x2="12.01" y2="8"/></svg>
</span>
<span class="nav-label">About</span>
</a>
<div class="admin-only" style="display:none">
<div class="sidebar-section-title">Admin</div>
<a href="dashboard.html" class="nav-item">
<span class="nav-icon">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 22c5.523 0 10-4.477 10-10S17.523 2 12 2 2 6.477 2 12s4.477 10 10 10z"/><path d="M2 12h20"/><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"/></svg>
</span>
<span class="nav-label">Admin Panel</span>
</a>
</div>
</nav>
<div class="sidebar-footer">
<div class="sidebar-user">
<div class="user-avatar" id="avatarInitial">U</div>
<div class="user-details">
<span class="user-name" id="user-name"></span>
<span class="user-email" id="user-email"></span>
</div>
</div>
<button class="logout-btn" id="adminToggle" onclick="toggleAdminMode()" style="display:none">
<span>
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 22c5.523 0 10-4.477 10-10S17.523 2 12 2 2 6.477 2 12s4.477 10 10 10z"/><path d="M2 12h20"/><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"/></svg>
</span>
<span id="adminToggleLabel">Admin View</span>
</button>
<button class="logout-btn" onclick="logout()">
<span>➜</span>
<span>Sign Out</span>
</button>
</div>
</aside>
<div class="main-content">
<div class="top-bar">
<button class="mobile-nav-toggle" onclick="document.getElementById('sidebar').classList.toggle('open')">☰</button>
</div>
<div class="page-content">
<div class="page-header">
<div>
<h2>Settings</h2>
<p class="subtitle">Configure your API keys and account</p>
</div>
</div>
<div class="card">
<div class="card-title">
<div class="card-icon">🔑</div>
AI Provider
</div>
<div class="form-group">
<label class="toggle-label">
<input type="checkbox" id="useDefaultApi" checked>
<span class="toggle-slider"></span>
<span><strong>Use default Nemotron AI (free)</strong><br>
<span style="font-weight:400;color:var(--text-subtle);font-size:12px">NVIDIA Nemotron 3 Ultra — free CV rewriting, editing, making & cover letters. No API key needed.</span></span>
</label>
</div>
<div id="personalKeysSection">
<p style="font-size:12px;color:var(--text-muted);margin-bottom:14px">
Provide your own API keys for AI-powered CV tailoring. Get free keys from
<a href="https://console.groq.com/keys" target="_blank">Groq</a>,
<a href="https://build.nvidia.com/" target="_blank">NVIDIA NIM</a>, or
<a href="https://aistudio.google.com/app/apikey" target="_blank">Google AI Studio</a>.
Your keys are stored encrypted and never shared.
</p>
<div class="form-group">
<label>Groq API Key <span style="font-weight:400;color:var(--text-subtle)">(fastest, free 14,400 req/day)</span></label>
<div class="key-input">
<span class="key-status" id="groqStatus"></span>
<input type="password" id="groqKey" placeholder="gsk_...">
</div>
</div>
<div class="form-group">
<label>NVIDIA NIM API Key <span style="font-weight:400;color:var(--text-subtle)">(fallback)</span></label>
<div class="key-input">
<span class="key-status" id="nvidiaStatus"></span>
<input type="password" id="nvidiaKey" placeholder="nvapi-...">
</div>
</div>
<div class="form-group">
<label>Gemini API Key <span style="font-weight:400;color:var(--text-subtle)">(2nd fallback)</span></label>
<div class="key-input">
<span class="key-status" id="geminiStatus"></span>
<input type="password" id="geminiKey" placeholder="AIza...">
</div>
</div>
</div>
<button class="btn btn-primary" onclick="saveKeys()" id="keysBtn">Save Settings</button>
</div>
<div class="card">
<div class="card-title">
<div class="card-icon">👤</div>
Account
</div>
<p style="font-size:13px;color:var(--text-muted)">Logged in as <strong id="accountEmail"></strong></p>
<p style="font-size:12px;color:var(--text-subtle);margin-top:4px">When "Use default Nemotron AI" is ON, all CV features use the free shared Nemotron model. Toggle off to use your personal API keys instead.</p>
</div>
</div>
</div>
</div>
<script src="app.js"></script>
<script>
async function loadKeys() {
try {
const keys = await apiFetch("GET", "/api/keys");
document.getElementById("groqKey").value = keys.groq || "";
document.getElementById("nvidiaKey").value = keys.nvidia || "";
document.getElementById("geminiKey").value = keys.gemini || "";
document.getElementById("useDefaultApi").checked = keys.use_default_api !== false;
updateStatus("groqStatus", !!keys.groq);
updateStatus("nvidiaStatus", !!keys.nvidia);
updateStatus("geminiStatus", !!keys.gemini);
togglePersonalSection();
} catch (err) { showToast(err.message, "error"); }
}
function updateStatus(elId, isSet) {
const el = document.getElementById(elId);
el.className = `key-status ${isSet ? "set" : "unset"}`;
el.title = isSet ? "Key is set" : "No key configured";
}
function togglePersonalSection() {
const useDefault = document.getElementById("useDefaultApi").checked;
const inputs = document.querySelectorAll("#personalKeysSection input");
for (const inp of inputs) inp.disabled = useDefault;
const section = document.getElementById("personalKeysSection");
section.style.opacity = useDefault ? "0.4" : "1";
section.style.pointerEvents = useDefault ? "none" : "auto";
}
document.getElementById("useDefaultApi").addEventListener("change", togglePersonalSection);
async function saveKeys() {
const btn = document.getElementById("keysBtn");
btn.disabled = true; btn.innerHTML = '<span class="spinner"></span> Saving...';
try {
const res = await apiFetch("PUT", "/api/keys", {
groq: document.getElementById("groqKey").value.trim(),
nvidia: document.getElementById("nvidiaKey").value.trim(),
gemini: document.getElementById("geminiKey").value.trim(),
use_default_api: document.getElementById("useDefaultApi").checked,
});
document.getElementById("groqKey").value = res.groq || "";
document.getElementById("nvidiaKey").value = res.nvidia || "";
document.getElementById("geminiKey").value = res.gemini || "";
document.getElementById("useDefaultApi").checked = res.use_default_api !== false;
updateStatus("groqStatus", !!res.groq);
updateStatus("nvidiaStatus", !!res.nvidia);
updateStatus("geminiStatus", !!res.gemini);
togglePersonalSection();
showToast("Settings saved");
} catch (err) { showToast(err.message, "error"); }
btn.disabled = false; btn.textContent = "Save Settings";
}
if (!getToken()) window.location.href = "login.html";
updateHeader();
const user = getCurrentUser();
if (user) document.getElementById("accountEmail").textContent = user.email || user.name;
loadKeys();
</script>
</body>
</html>
|