JavaScript
challenge13-webdev-js
JavaScript
challenge14-webdev-chatbot
1 /* ───────────────────────────────────────── 2 TOKENS 3 ───────────────────────────────────────── */ 4 :root { 5 --bg: #07140d; 6 --surface: #0e2118; 7 --mid: #163326; 8 --lime: #c6f135; 9 --lime-dim: rgba(198,241,53,.12); 10 --lime-border: rgba(198,241,53,.22); 11 --teal: #34e8b0; 12 --teal-dim: rgba(52,232,176,.1); 13 --cream: #eef9ee; 14 --muted: rgba(238,249,238,.45); 15 --divider: rgba(198,241,53,.1); 16 --red-dim: rgba(255,100,100,.12); 17 --red-border: rgba(255,100,100,.35); 18 } 19 20 *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; } 21 22 html { scroll-behavior: smooth; } 23 24 body { 25 font-family: 'Plus Jakarta Sans', sans-serif; 26 background: var(--bg); 27 color: var(--cream); 28 min-height: 100vh; 29 overflow-x: hidden; 30 } 31 32 /* ─── Ambient background ─── */ 33 .ambient { 34 position: fixed; inset: 0; z-index: 0; pointer-events: none; 35 overflow: hidden; 36 } 37 .amb-blob { 38 position: absolute; 39 border-radius: 50%; 40 filter: blur(90px); 41 opacity: .18; 42 animation: blobDrift ease-in-out infinite alternate; 43 } 44 .amb-blob:nth-child(1){width:520px;height:520px;background:#1a7a3a;top:-120px;left:-100px;animation-duration:14s;} 45 .amb-blob:nth-child(2){width:420px;height:420px;background:#0a5c3a;bottom:-80px;right:-80px;animation-duration:18s;opacity:.14;} 46 .amb-blob:nth-child(3){width:280px;height:280px;background:#c6f135;top:40%;left:60%;animation-duration:11s;opacity:.06;} 47 48 @keyframes blobDrift { 49 from { transform: translate(0,0) scale(1); } 50 to { transform: translate(30px,20px) scale(1.08); } 51 } 52 53 /* ─── Noise grain overlay ─── */ 54 .grain { 55 position: fixed; inset: 0; z-index: 1; pointer-events: none; 56 background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 256 256' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='noise'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23noise)' opacity='1'/%3E%3C/svg%3E"); 57 opacity: .025; 58 } 59 60 /* ───────────────────────────────────────── 61 LAYOUT 62 ───────────────────────────────────────── */ 63 .page { 64 position: relative; z-index: 2; 65 max-width: 820px; 66 margin: 0 auto; 67 padding: 0 28px 80px; 68 } 69 70 /* ───────────────────────────────────────── 71 HERO HEADER — full bleed, not a card 72 ───────────────────────────────────────── */ 73 .hero { 74 padding: 48px 0 32px; 75 text-align: center; 76 animation: fadeUp .8s cubic-bezier(.22,1,.36,1) both; 77 } 78 79 .hero-pill { 80 display: inline-flex; 81 align-items: center; 82 gap: 7px; 83 background: var(--lime-dim); 84 border: 1px solid var(--lime-border); 85 border-radius: 999px; 86 padding: 5px 16px; 87 font-size: .72rem; 88 font-weight: 700; 89 letter-spacing: .12em; 90 text-transform: uppercase; 91 color: var(--lime); 92 margin-bottom: 22px; 93 } 94 95 .hero-pill .pulse-dot { 96 width: 6px; height: 6px; 97 background: var(--lime); 98 border-radius: 50%; 99 box-shadow: 0 0 0 0 rgba(198,241,53,.6); 100 animation: pulseDot 2s ease-out infinite; 101 } 102 103 @keyframes pulseDot { 104 0% { box-shadow: 0 0 0 0 rgba(198,241,53,.6); } 105 70% { box-shadow: 0 0 0 8px rgba(198,241,53,0); } 106 100% { box-shadow: 0 0 0 0 rgba(198,241,53,0); } 107 } 108 109 .hero h1 { 110 font-family: 'Instrument Serif', serif; 111 font-size: clamp(2.2rem, 6vw, 3.4rem); 112 line-height: 1.1; 113 color: var(--cream); 114 margin-bottom: 10px; 115 } 116 117 .hero h1 em { 118 font-style: italic; 119 color: var(--lime); 120 } 121 122 .hero p { 123 font-size: .95rem; 124 color: var(--muted); 125 font-weight: 400; 126 max-width: 380px; 127 margin: 0 auto; 128 line-height: 1.7; 129 } 130 131 /* ───────────────────────────────────────── 132 FORM AREA — single unified surface 133 ───────────────────────────────────────── */ 134 .form-surface { 135 background: var(--surface); 136 border: 1px solid rgba(255,255,255,.06); 137 border-radius: 28px; 138 padding: 32px 40px; 139 animation: fadeUp .8s .15s cubic-bezier(.22,1,.36,1) both; 140 } 141 142 /* ─── Field label ─── */ 143 .field-label { 144 font-size: .72rem; 145 font-weight: 700; 146 letter-spacing: .1em; 147 text-transform: uppercase; 148 color: var(--muted); 149 margin-bottom: 10px; 150 display: block; 151 } 152 153 /* ─── Divider between fields ─── */ 154 .field-divider { 155 border: none; 156 border-top: 1px solid var(--divider); 157 margin: 26px 0; 158 } 159 160 /* ─── TEXTAREA ─── */ 161 .habit-textarea { 162 width: 100%; 163 background: transparent; 164 border: none; 165 border-bottom: 1.5px solid rgba(255,255,255,.1); 166 border-radius: 0; 167 padding: 10px 0 14px; 168 font-size: 0.95rem; 169 font-family: 'Plus Jakarta Sans', sans-serif; 170 color: var(--cream); 171 resize: none; 172 outline: none; 173 min-height: 100px; 174 height: 100px; 175 line-height: 1.7; 176 transition: border-color .3s; 177 } 178 179 .habit-textarea::placeholder { color: rgba(238,249,238,.22); } 180 181 .habit-textarea:focus { 182 border-bottom-color: var(--lime); 183 } 184 185 .textarea-footer { 186 display: flex; 187 justify-content: space-between; 188 align-items: center; 189 margin-top: 6px; 190 } 191 192 .char-hint { 193 font-size: .72rem; 194 color: rgba(238,249,238,.25); 195 font-weight: 500; 196 transition: color .2s; 197 } 198 .char-hint.active { color: var(--teal); } 199 200 .clear-btn { 201 background: none; border: none; padding: 0; 202 font-size: .72rem; font-weight: 600; 203 color: rgba(238,249,238,.2); 204 cursor: pointer; 205 transition: color .2s; 206 font-family: inherit; 207 } 208 .clear-btn:hover { color: var(--muted); } 209 210 /* ─── TOPIC CHIPS ─── */ 211 .chips-row { 212 display: flex; 213 flex-wrap: wrap; 214 gap: 8px; 215 } 216 217 .chip { 218 display: inline-flex; 219 align-items: center; 220 gap: 5px; 221 background: transparent; 222 border: 1px solid rgba(255,255,255,.1); 223 border-radius: 999px; 224 padding: 6px 16px; 225 font-size: .8rem; 226 font-weight: 600; 227 color: var(--muted); 228 font-family: 'Plus Jakarta Sans', sans-serif; 229 cursor: pointer; 230 transition: all .2s cubic-bezier(.22,1,.36,1); 231 user-select: none; 232 } 233 234 .chip:hover { 235 border-color: rgba(198,241,53,.4); 236 color: var(--cream); 237 background: var(--lime-dim); 238 transform: translateY(-2px); 239 } 240 241 .chip.active { 242 background: var(--lime-dim); 243 border-color: var(--lime); 244 color: var(--lime); 245 font-weight: 700; 246 } 247 248 /* ─── GENERATE BUTTON ─── */ 249 .btn-generate { 250 width: 100%; 251 margin-top: 20px; 252 padding: 14px 24px; 253 background: var(--lime); 254 color: #0a1a0d; 255 border: none; 256 border-radius: 16px; 257 font-size: 1rem; 258 font-weight: 800; 259 font-family: 'Plus Jakarta Sans', sans-serif; 260 letter-spacing: .02em; 261 cursor: pointer; 262 position: relative; 263 overflow: hidden; 264 transition: transform .2s, box-shadow .2s, background .2s; 265 box-shadow: 0 4px 32px rgba(198,241,53,.2); 266 } 267 268 .btn-generate::after { 269 content: ''; 270 position: absolute; 271 inset: 0; 272 background: linear-gradient(90deg, transparent 0%, rgba(255,255,255,.25) 50%, transparent 100%); 273 transform: translateX(-100%); 274 transition: transform .5s ease; 275 } 276 277 .btn-generate:hover:not(:disabled)::after { transform: translateX(100%); } 278 279 .btn-generate:hover:not(:disabled) { 280 transform: translateY(-2px); 281 box-shadow: 0 8px 40px rgba(198,241,53,.35); 282 background: #d4f747; 283 } 284 285 .btn-generate:active:not(:disabled) { transform: translateY(0); } 286 287 .btn-generate:disabled { 288 opacity: .5; 289 cursor: not-allowed; 290 } 291 292 /* ───────────────────────────────────────── 293 RESULT SECTION — scrollable area 294 ───────────────────────────────────────── */ 295 #result-area { 296 margin-top: 24px; 297 max-height: 400px; 298 overflow-y: auto; 299 padding-right: 8px; 300 animation: fadeUp .6s cubic-bezier(.22,1,.36,1) both; 301 } 302 303 /* ─── Result header row ─── */ 304 .result-header { 305 display: flex; 306 align-items: center; 307 gap: 10px; 308 margin-bottom: 20px; 309 } 310 311 .result-header-line { 312 flex: 1; 313 height: 1px; 314 background: var(--divider); 315 } 316 317 .result-header-label { 318 font-size: .7rem; 319 font-weight: 700; 320 letter-spacing: .12em; 321 text-transform: uppercase; 322 color: rgba(238,249,238,.3); 323 white-space: nowrap; 324 } 325 326 /* ─── Each result row ─── */ 327 .result-row { 328 padding: 0 0 24px; 329 animation: fadeUp .5s cubic-bezier(.22,1,.36,1) both; 330 } 331 332 .result-row + .result-row { 333 border-top: 1px solid var(--divider); 334 padding-top: 24px; 335 } 336 337 .result-row:nth-child(2) { animation-delay: .1s; } 338 339 .rr-type { 340 display: inline-flex; 341 align-items: center; 342 gap: 6px; 343 font-size: .7rem; 344 font-weight: 700; 345 letter-spacing: .1em; 346 text-transform: uppercase; 347 margin-bottom: 10px; 348 } 349 350 .rr-type.q { color: var(--lime); } 351 .rr-type.s { color: var(--teal); } 352 353 .rr-icon { 354 width: 22px; height: 22px; 355 border-radius: 6px; 356 display: inline-flex; 357 align-items: center; 358 justify-content: center; 359 font-size: .75rem; 360 } 361 362 .rr-type.q .rr-icon { background: rgba(198,241,53,.15); color: var(--lime); } 363 .rr-type.s .rr-icon { background: rgba(52,232,176,.12); color: var(--teal); } 364 365 .rr-text { 366 font-family: 'Instrument Serif', serif; 367 font-size: 1.12rem; 368 line-height: 1.65; 369 color: var(--cream); 370 } 371 372 /* ─── Try again row ─── */ 373 .try-again-row { 374 margin-top: 24px; 375 display: flex; 376 justify-content: center; 377 animation: fadeUp .5s .2s cubic-bezier(.22,1,.36,1) both; 378 } 379 380 .btn-try-again { 381 background: none; 382 border: 1px solid rgba(255,255,255,.12); 383 border-radius: 999px; 384 padding: 8px 22px; 385 font-size: .82rem; 386 font-weight: 600; 387 color: var(--muted); 388 font-family: 'Plus Jakarta Sans', sans-serif; 389 cursor: pointer; 390 transition: all .2s; 391 display: inline-flex; 392 align-items: center; 393 gap: 7px; 394 } 395 396 .btn-try-again:hover { 397 border-color: rgba(198,241,53,.3); 398 color: var(--cream); 399 background: var(--lime-dim); 400 } 401 402 /* ───────────────────────────────────────── 403 LOADING STATE 404 ───────────────────────────────────────── */ 405 .loading-state { 406 display: flex; 407 flex-direction: column; 408 align-items: center; 409 gap: 16px; 410 padding: 40px 0; 411 } 412 413 .loader-track { 414 width: 180px; 415 height: 2px; 416 background: rgba(255,255,255,.07); 417 border-radius: 2px; 418 overflow: hidden; 419 } 420 421 .loader-fill { 422 height: 100%; 423 background: linear-gradient(90deg, var(--lime), var(--teal)); 424 border-radius: 2px; 425 animation: loaderSweep 1.6s ease-in-out infinite; 426 transform-origin: left; 427 } 428 429 @keyframes loaderSweep { 430 0% { transform: scaleX(0) translateX(0%); } 431 50% { transform: scaleX(1) translateX(0%); } 432 100% { transform: scaleX(0) translateX(200%); } 433 } 434 435 .loading-state p { 436 font-size: .82rem; 437 color: var(--muted); 438 font-weight: 500; 439 } 440 441 /* ───────────────────────────────────────── 442 ERROR STATE 443 ───────────────────────────────────────── */ 444 .error-state { 445 background: var(--red-dim); 446 border: 1px solid var(--red-border); 447 border-radius: 14px; 448 padding: 16px 20px; 449 font-size: .9rem; 450 color: #ffaaaa; 451 display: flex; 452 align-items: flex-start; 453 gap: 10px; 454 animation: fadeUp .4s ease; 455 } 456 457 .error-state i { flex-shrink: 0; font-size: 1rem; margin-top: 1px; } 458 459 /* ───────────────────────────────────────── 460 FOOTER 461 ───────────────────────────────────────── */ 462 .footer { 463 text-align: center; 464 padding: 0 0 32px; 465 font-size: .72rem; 466 color: rgba(238,249,238,.18); 467 font-weight: 500; 468 position: relative; z-index: 2; 469 } 470 471 .footer a { color: rgba(198,241,53,.45); text-decoration: none; } 472 .footer a:hover { color: var(--lime); } 473 474 /* ───────────────────────────────────────── 475 UTILITIES 476 ───────────────────────────────────────── */ 477 @keyframes fadeUp { 478 from { opacity:0; transform:translateY(20px); } 479 to { opacity:1; transform:translateY(0); } 480 } 481 482 /* Scrollbar */ 483 ::-webkit-scrollbar { width: 4px; } 484 ::-webkit-scrollbar-track { background: transparent; } 485 ::-webkit-scrollbar-thumb { background: var(--mid); border-radius: 4px; } 486 487 /* Mobile */ 488 @media (max-width: 480px) { 489 .form-surface { padding: 24px 18px; border-radius: 22px; } 490 .hero { padding: 36px 0 28px; } 491 .hero h1 { font-size: 2rem; } 492 .page { padding: 0 16px 60px; } 493 #result-area { max-height: 300px; } 494 }
1 <!DOCTYPE html> 2 <html lang="en"> 3 <head> 4 <meta charset="UTF-8"/> 5 <meta name="viewport" content="width=device-width, initial-scale=1.0"/> 6 <title>AI Lifestyle Reflection Generator</title> 7 8 <link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" rel="stylesheet"/> 9 <link href="https://cdn.jsdelivr.net/npm/[email protected]/font/bootstrap-icons.min.css" rel="stylesheet"/> 10 <link href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@300;400;500;600;700;800&family=Instrument+Serif:ital@0;1&display=swap" rel="stylesheet"/> 11 <link rel="stylesheet" href="style.css"> 12 </head> 13 <body> 14 15 <!-- Ambient background blobs --> 16 <div class="ambient"> 17 <div class="amb-blob"></div> 18 <div class="amb-blob"></div> 19 <div class="amb-blob"></div> 20 </div> 21 <!-- Grain texture --> 22 <div class="grain"></div> 23 24 <div class="page"> 25 26 <!-- ═══════════════════════════════════════ 27 HERO 28 ════════════════════════════════════════ --> 29 <header class="hero"> 30 <div class="hero-pill"> 31 <span class="pulse-dot"></span> 32 Powered by Gemini AI 33 </div> 34 <h1>Reflect on your<br><em>lifestyle habits</em></h1> 35 <p>Describe how you live, and let AI help you think deeper about your daily choices.</p> 36 </header> 37 38 <!-- ═══════════════════════════════════════ 39 UNIFIED FORM SURFACE 40 ════════════════════════════════════════ --> 41 <div class="form-surface"> 42 43 <!-- ── Describe Your Lifestyle ── --> 44 <label class="field-label" for="habitInput">Describe your lifestyle</label> 45 <textarea 46 class="habit-textarea" 47 id="habitInput" 48 rows="7" 49 placeholder="e.g. I sleep only 5 hours a night, scroll my phone before bed, skip breakfast, and sit at my desk all day without breaks…" 50 oninput="onType(this)" 51 ></textarea> 52 <div class="textarea-footer"> 53 <span class="char-hint" id="charHint">Start typing…</span> 54 <button class="clear-btn" onclick="clearInput()" id="clearBtn" style="display:none">Clear</button> 55 </div> 56 57 <!-- ── Divider ── --> 58 <hr class="field-divider"/> 59 60 <!-- ── Categories ── --> 61 <label class="field-label">Quick topics</label> 62 <div class="chips-row"> 63 <button class="chip" onclick="fillTopic(this,'study')">📚 Study Habits</button> 64 <button class="chip" onclick="fillTopic(this,'sleep')">😴 Sleep</button> 65 <button class="chip" onclick="fillTopic(this,'screen')">📱 Screen Time</button> 66 <button class="chip" onclick="fillTopic(this,'exercise')">🏃 Exercise</button> 67 </div> 68 69 <!-- ── Generate Button ── --> 70 <button class="btn-generate" id="generateBtn" onclick="generateReflection()"> 71 <i class="bi bi-stars me-2"></i>Generate Reflection 72 </button> 73 74 </div><!-- /form-surface --> 75 76 <!-- ═══════════════════════════════════════ 77 RESULT AREA — flows naturally below 78 ════════════════════════════════════════ --> 79 <div id="result-area" style="display:none"></div> 80 81 </div><!-- /page --> 82 83 <!-- Footer --> 84 <footer class="footer"> 85 <p>AI Lifestyle Reflection Generator · 86 Powered by <a href="https://deepmind.google/technologies/gemini/" target="_blank">Google Gemini</a> 87 · Bootstrap 5</p> 88 </footer> 89 90 <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.bundle.min.js"></script> 91 <script src="script.js"></script> 92 </body> 93 </html>
1 // ====================================== 2 // API KEY FOR GOOGLE GEMINI 3 // Get your own from: https://aistudio.google.com/app/apikey 4 // ====================================== 5 const GEMINI_API_KEY = ""; 6 7 // ====================================== 8 // SAMPLE TOPICS 9 // ====================================== 10 const TOPICS = { 11 study: "I usually study late at night, often past midnight. I struggle to stay focused and feel exhausted the next morning.", 12 sleep: "I only sleep 5–6 hours on weekdays. I scroll my phone for at least an hour before bed every night.", 13 screen: "I spend 8 hours on a computer for work, then another 3–4 hours at night on my phone watching videos and browsing.", 14 exercise: "I barely exercise — maybe once or twice a month. I sit at my desk all day and take the elevator instead of the stairs." 15 }; 16 17 // Rate limiting variables 18 let lastRequestTime = 0; 19 const MIN_REQUEST_INTERVAL = 5000; // Wait 5 seconds between requests 20 21 22 // ====================================== 23 // FILL TOPIC - When user clicks a sample topic button 24 // ====================================== 25 function fillTopic(clickedButton, topicKey) { 26 // Get the text input field 27 const inputField = document.getElementById('habitInput'); 28 29 // Put the sample text into the input 30 inputField.value = TOPICS[topicKey]; 31 32 // Update the character counter 33 onType(inputField); 34 35 // Remove active style from all topic buttons 36 const allButtons = document.querySelectorAll('.chip'); 37 for (let i = 0; i < allButtons.length; i = i + 1) { 38 allButtons[i].classList.remove('active'); 39 } 40 41 // Add active style to the clicked button 42 clickedButton.classList.add('active'); 43 } 44 45 46 // ====================================== 47 // ON TYPE - Update character counter when typing 48 // ====================================== 49 function onType(inputElement) { 50 // Get the hint text element 51 const hintElement = document.getElementById('charHint'); 52 53 // Get the clear button 54 const clearButton = document.getElementById('clearBtn'); 55 56 // Count the characters typed 57 const textLength = inputElement.value.trim().length; 58 59 // Update the hint text 60 if (textLength === 0) { 61 hintElement.textContent = 'Start typing…'; 62 } else { 63 hintElement.textContent = inputElement.value.length + ' characters'; 64 } 65 66 // Show/hide the hint 67 if (textLength > 0) { 68 hintElement.classList.add('active'); 69 } else { 70 hintElement.classList.remove('active'); 71 } 72 73 // Show/hide the clear button 74 if (textLength > 0) { 75 clearButton.style.display = 'inline'; 76 } else { 77 clearButton.style.display = 'none'; 78 } 79 } 80 81 82 // ====================================== 83 // CLEAR INPUT - Empty the text field 84 // ====================================== 85 function clearInput() { 86 // Get the input field 87 const inputField = document.getElementById('habitInput'); 88 89 // Clear the text 90 inputField.value = ''; 91 92 // Update the counter 93 onType(inputField); 94 95 // Remove active style from all topic buttons 96 const allButtons = document.querySelectorAll('.chip'); 97 for (let i = 0; i < allButtons.length; i = i + 1) { 98 allButtons[i].classList.remove('active'); 99 } 100 101 // Focus on the input field 102 inputField.focus(); 103 } 104 105 106 107 // ====================================== 108 // GENERATE REFLECTION - Main function 109 // ====================================== 110 function generateReflection() { 111 // Get the text from input 112 const userText = document.getElementById('habitInput').value.trim(); 113 114 // Check if input is empty 115 if (userText === '') { 116 shakeForm(); 117 return; 118 } 119 120 // Check if we should wait before making another request 121 const currentTime = Date.now(); 122 const timePassed = currentTime - lastRequestTime; 123 124 if (timePassed < MIN_REQUEST_INTERVAL) { 125 const waitSeconds = Math.ceil((MIN_REQUEST_INTERVAL - timePassed) / 1000); 126 alert('Please wait ' + waitSeconds + ' seconds before trying again.'); 127 return; 128 } 129 130 // Save the time of this request 131 lastRequestTime = currentTime; 132 133 // Show loading state 134 setBtnLoading(true); 135 showLoading(); 136 137 // Create the message to send to Gemini 138 const message = 'You are a helpful lifestyle coach. The user says: ' + userText + 139 ' Give one reflection question and one improvement suggestion in simple JSON format.'; 140 141 // Send request to Gemini API 142 sendToGemini(message); 143 } 144 145 146 // ====================================== 147 // SEND TO GEMINI - Make API request 148 // ====================================== 149 function sendToGemini(message) { 150 // Prepare the request data 151 const requestData = { 152 contents: [ 153 { 154 parts: [ 155 { 156 text: message 157 } 158 ] 159 } 160 ] 161 }; 162 163 // Make the API request 164 fetch( 165 'https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent?key=' + GEMINI_API_KEY, 166 { 167 method: 'POST', 168 headers: { 169 'Content-Type': 'application/json' 170 }, 171 body: JSON.stringify(requestData) 172 } 173 ) 174 .then(function(response) { 175 // Check if response is OK 176 if (response.ok) { 177 return response.json(); 178 } else { 179 throw new Error('API returned status ' + response.status); 180 } 181 }) 182 .then(function(data) { 183 // Get the text response from Gemini 184 const textResponse = data.candidates[0].content.parts[0].text; 185 186 // Try to parse the JSON 187 const result = JSON.parse(textResponse); 188 189 // Show the results 190 showResult(result.question, result.suggestion); 191 }) 192 .catch(function(error) { 193 // Show error message 194 showError('Error: ' + error.message); 195 }) 196 .finally(function() { 197 // Reset button 198 setBtnLoading(false); 199 }); 200 } 201 202 203 204 205 // ====================================== 206 // SET BUTTON LOADING - Show/hide loading state on button 207 // ====================================== 208 function setBtnLoading(isLoading) { 209 const button = document.getElementById('generateBtn'); 210 211 if (isLoading) { 212 button.disabled = true; 213 button.innerHTML = '<span>Thinking…</span>'; 214 } else { 215 button.disabled = false; 216 button.innerHTML = '<i class="bi bi-stars me-2"></i>Generate Reflection'; 217 } 218 } 219 220 221 // ====================================== 222 // SHOW LOADING - Display loading animation 223 // ====================================== 224 function showLoading() { 225 const resultArea = document.getElementById('result-area'); 226 227 resultArea.style.display = 'block'; 228 resultArea.innerHTML = '<div class="loading-state"><div class="loader-track"><div class="loader-fill"></div></div><p>Gemini is reflecting on your lifestyle…</p></div>'; 229 230 // Scroll to show the loading message 231 resultArea.scrollIntoView({ behavior: 'smooth', block: 'nearest' }); 232 } 233 234 235 // ====================================== 236 // SHOW RESULT - Display the AI response 237 // ====================================== 238 function showResult(question, suggestion) { 239 const resultArea = document.getElementById('result-area'); 240 241 // Create the HTML to show 242 const html = '<div class="result-header"><div class="result-header-line"></div><span class="result-header-label">Your reflection</span><div class="result-header-line"></div></div>' + 243 '<div class="result-row"><div class="rr-type q"><span class="rr-icon"><i class="bi bi-question-lg"></i></span>Reflection Question</div><p class="rr-text">' + question + '</p></div>' + 244 '<div class="result-row"><div class="rr-type s"><span class="rr-icon"><i class="bi bi-lightbulb-fill"></i></span>Suggestion for Improvement</div><p class="rr-text">' + suggestion + '</p></div>' + 245 '<div class="try-again-row"><button class="btn-try-again" onclick="resetAll()"><i class="bi bi-arrow-counterclockwise"></i> Try another habit</button></div>'; 246 247 resultArea.style.display = 'block'; 248 resultArea.innerHTML = html; 249 250 // Scroll to show the result 251 resultArea.scrollIntoView({ behavior: 'smooth', block: 'nearest' }); 252 } 253 254 255 // ====================================== 256 // SHOW ERROR - Display error message 257 // ====================================== 258 function showError(errorMessage) { 259 const resultArea = document.getElementById('result-area'); 260 261 const html = '<div class="error-state"><i class="bi bi-exclamation-triangle-fill"></i><span>' + errorMessage + '</span></div>'; 262 263 resultArea.style.display = 'block'; 264 resultArea.innerHTML = html; 265 } 266 267 268 // ====================================== 269 // RESET ALL - Clear everything and start fresh 270 // ====================================== 271 function resetAll() { 272 // Clear input field 273 clearInput(); 274 275 // Hide results 276 const resultArea = document.getElementById('result-area'); 277 resultArea.style.display = 'none'; 278 resultArea.innerHTML = ''; 279 280 // Scroll back to top 281 window.scrollTo({ top: 0, behavior: 'smooth' }); 282 } 283 284 285 // ====================================== 286 // SHAKE FORM - Animated shake when input is empty 287 // ====================================== 288 function shakeForm() { 289 // Get the form element 290 const form = document.querySelector('.form-surface'); 291 292 // Reset animation 293 form.style.animation = 'none'; 294 form.style.transform = 'translateX(0)'; 295 296 // Variables for shake animation 297 let shakeCount = 0; 298 let shakeDirection = 1; 299 300 // Create shake effect 301 const shakeInterval = setInterval(function() { 302 form.style.transform = 'translateX(' + (shakeDirection * 5) + 'px)'; 303 shakeDirection = shakeDirection * -1; 304 shakeCount = shakeCount + 1; 305 306 // Stop after 8 shakes 307 if (shakeCount > 7) { 308 clearInterval(shakeInterval); 309 form.style.transform = 'none'; 310 } 311 }, 55); 312 313 // Get the input field 314 const inputField = document.getElementById('habitInput'); 315 316 // Turn the border red 317 inputField.style.borderBottomColor = '#ff6464'; 318 319 // Turn it back to normal after 1.2 seconds 320 setTimeout(function() { 321 inputField.style.borderBottomColor = ''; 322 }, 1200); 323 324 // Focus on the input 325 inputField.focus(); 326 }
1 // ============================================================ 2 // Gadjah Mada High School — AI Assistant Widget 3 // Powered by Claude (Anthropic API via claude.ai proxy) 4 // ============================================================ 5 6 (function () { 7 /* ---- School context given to the AI ---- */ 8 const SYSTEM_PROMPT = `You are the official AI assistant for Gadjah Mada High School (SMA Gadjah Mada), an elite high school established in 1985 in West Jakarta, Indonesia. 9 10 You help visitors, prospective students, parents, and community members with questions about the school. 11 12 Key facts about the school: 13 - Founded: 1985 | Location: Jendral Soedirman 12, West Jakarta 14 - Phone: (+62)812-3100-3000 | Office hours: Mon–Fri, 7:30 AM – 5:00 PM 15 - School hours: Mon–Fri 7:00 AM – 4:30 PM 16 - 98% university admission rate | 2,800+ students | 120+ expert faculty | 85+ awards 17 - Accredited and award-winning (2025) 18 - Programs: STEM, Humanities, Arts & Music, Sports, University Prep 19 - Student orgs: Student Council (OSIS), Science Club, Arts Collective, Sports Federation, Debate Society, Robotics Club 20 - Notable achievements: National Academic Excellence Award (2020–2025), National Robotics Champions (2025), UNESCO School of the Year APAC (2023), Best Performing Arts (2024), State Multi-Sport Champions (2024), Green School Platinum Certification (2023) 21 22 Admissions: 23 - Open Days are scheduled — direct inquiries to [email protected] 24 - Scholarships available — contact the admissions office 25 - For tuition, fees, and applications, direct visitors to the Contact section or call the office 26 27 Tone: warm, professional, proud of the school. Always respond in English only, regardless of the language the user writes in. Keep answers concise but helpful. If you don't know something specific, kindly direct them to contact the school directly.`; 28 29 /* ---- Inject CSS ---- */ 30 const style = document.createElement('style'); 31 style.textContent = ` 32 :root { 33 --cb-navy: #0B1F3A; 34 --cb-gold: #C9A84C; 35 --cb-gold-light: #F0D080; 36 --cb-cream: #FAF7F0; 37 } 38 #gm-chat-fab { 39 position: fixed; bottom: 28px; right: 28px; z-index: 9999; 40 width: 60px; height: 60px; border-radius: 50%; 41 background: var(--cb-gold); border: none; cursor: pointer; 42 box-shadow: 0 8px 32px rgba(201,168,76,.45), 0 2px 8px rgba(0,0,0,.2); 43 display: flex; align-items: center; justify-content: center; 44 transition: transform .25s, box-shadow .25s; 45 } 46 #gm-chat-fab:hover { transform: scale(1.08) translateY(-2px); box-shadow: 0 16px 48px rgba(201,168,76,.5), 0 4px 12px rgba(0,0,0,.25); } 47 #gm-chat-fab svg { pointer-events: none; } 48 49 #gm-chat-badge { 50 position: absolute; top: -3px; right: -3px; 51 width: 18px; height: 18px; background: #e63946; border-radius: 50%; 52 border: 2px solid #fff; display: flex; align-items: center; justify-content: center; 53 font-size: 10px; color: #fff; font-weight: 700; font-family: 'DM Sans', sans-serif; 54 animation: gm-pulse 2s infinite; 55 } 56 @keyframes gm-pulse { 0%, 100% { transform: scale(1); } 50% { transform: scale(1.15); } } 57 58 #gm-chat-window { 59 position: fixed; bottom: 100px; right: 28px; z-index: 9998; 60 width: 380px; max-width: calc(100vw - 40px); 61 height: 560px; max-height: calc(100vh - 130px); 62 background: var(--cb-navy); border-radius: 18px; 63 border: 1.5px solid rgba(201,168,76,.35); 64 box-shadow: 0 32px 80px rgba(0,0,0,.5), 0 0 0 1px rgba(201,168,76,.1); 65 display: flex; flex-direction: column; overflow: hidden; 66 font-family: 'DM Sans', sans-serif; transform-origin: bottom right; 67 transition: transform .3s cubic-bezier(.34,1.56,.64,1), opacity .3s; 68 } 69 #gm-chat-window.gm-hidden { transform: scale(.85) translateY(20px); opacity: 0; pointer-events: none; } 70 71 .gm-chat-header { 72 background: linear-gradient(135deg, #0e2845 0%, #0B1F3A 100%); 73 border-bottom: 1.5px solid rgba(201,168,76,.3); padding: 16px 18px; 74 display: flex; align-items: center; gap: 12px; flex-shrink: 0; 75 } 76 .gm-header-avatar { 77 width: 40px; height: 40px; border-radius: 50%; 78 background: var(--cb-gold); display: flex; align-items: center; justify-content: center; 79 font-family: 'Playfair Display', serif; font-weight: 900; font-size: 17px; 80 color: var(--cb-navy); flex-shrink: 0; position: relative; 81 } 82 .gm-header-avatar::after { 83 content: ''; position: absolute; bottom: 1px; right: 1px; 84 width: 10px; height: 10px; background: #2dc653; border-radius: 50%; 85 border: 2px solid var(--cb-navy); 86 } 87 .gm-header-info { flex: 1; min-width: 0; } 88 .gm-header-name { color: #fff; font-weight: 600; font-size: 14px; letter-spacing: .3px; } 89 .gm-header-status { color: rgba(255,255,255,.45); font-size: 11px; letter-spacing: .5px; } 90 .gm-close-btn { 91 background: rgba(255,255,255,.07); border: none; width: 30px; height: 30px; 92 border-radius: 50%; cursor: pointer; display: flex; align-items: center; justify-content: center; 93 color: rgba(255,255,255,.5); transition: background .2s, color .2s; flex-shrink: 0; 94 } 95 .gm-close-btn:hover { background: rgba(255,255,255,.14); color: #fff; } 96 97 .gm-messages { 98 flex: 1; overflow-y: auto; padding: 18px 16px; 99 display: flex; flex-direction: column; gap: 12px; 100 scrollbar-width: thin; scrollbar-color: rgba(201,168,76,.3) transparent; 101 } 102 .gm-messages::-webkit-scrollbar { width: 4px; } 103 .gm-messages::-webkit-scrollbar-thumb { background: rgba(201,168,76,.3); border-radius: 4px; } 104 105 .gm-msg { display: flex; flex-direction: column; max-width: 88%; animation: gm-msg-in .3s ease both; } 106 @keyframes gm-msg-in { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: translateY(0); } } 107 .gm-msg.gm-user { align-self: flex-end; align-items: flex-end; } 108 .gm-msg.gm-bot { align-self: flex-start; align-items: flex-start; } 109 110 .gm-bubble { padding: 11px 14px; border-radius: 14px; font-size: 13.5px; line-height: 1.6; word-break: break-word; } 111 .gm-user .gm-bubble { background: var(--cb-gold); color: var(--cb-navy); font-weight: 500; border-bottom-right-radius: 4px; } 112 .gm-bot .gm-bubble { background: rgba(255,255,255,.07); color: rgba(255,255,255,.88); border: 1px solid rgba(255,255,255,.1); border-bottom-left-radius: 4px; } 113 .gm-time { font-size: 10px; color: rgba(255,255,255,.25); margin-top: 4px; padding: 0 4px; } 114 115 .gm-typing .gm-bubble { display: flex; align-items: center; gap: 4px; padding: 14px 16px; } 116 .gm-dot { width: 7px; height: 7px; background: var(--cb-gold); border-radius: 50%; animation: gm-bounce 1.2s infinite ease-in-out; } 117 .gm-dot:nth-child(2) { animation-delay: .15s; } 118 .gm-dot:nth-child(3) { animation-delay: .3s; } 119 @keyframes gm-bounce { 0%, 60%, 100% { transform: translateY(0); opacity: .5; } 30% { transform: translateY(-5px); opacity: 1; } } 120 121 .gm-suggestions { display: flex; flex-wrap: wrap; gap: 6px; padding: 0 16px 12px; flex-shrink: 0; } 122 .gm-chip { 123 background: rgba(201,168,76,.12); border: 1px solid rgba(201,168,76,.3); 124 color: var(--cb-gold); font-size: 11px; padding: 5px 12px; border-radius: 20px; 125 cursor: pointer; font-family: 'DM Sans', sans-serif; letter-spacing: .3px; 126 transition: background .2s, border-color .2s; white-space: nowrap; 127 } 128 .gm-chip:hover { background: rgba(201,168,76,.25); border-color: rgba(201,168,76,.6); } 129 130 .gm-input-row { display: flex; align-items: center; gap: 8px; padding: 12px 14px; border-top: 1px solid rgba(255,255,255,.08); background: rgba(0,0,0,.2); flex-shrink: 0; } 131 .gm-input { 132 flex: 1; background: rgba(255,255,255,.07); border: 1px solid rgba(255,255,255,.12); 133 border-radius: 24px; padding: 9px 16px; color: #fff; font-size: 13.5px; 134 font-family: 'DM Sans', sans-serif; outline: none; transition: border-color .2s; 135 resize: none; line-height: 1.4; max-height: 90px; overflow-y: auto; 136 } 137 .gm-input::placeholder { color: rgba(255,255,255,.28); } 138 .gm-input:focus { border-color: rgba(201,168,76,.5); } 139 .gm-send-btn { 140 width: 38px; height: 38px; border-radius: 50%; 141 background: var(--cb-gold); border: none; cursor: pointer; 142 display: flex; align-items: center; justify-content: center; 143 transition: background .2s, transform .15s; flex-shrink: 0; 144 } 145 .gm-send-btn:hover:not(:disabled) { background: var(--cb-gold-light); transform: scale(1.07); } 146 .gm-send-btn:disabled { opacity: .45; cursor: not-allowed; transform: none; } 147 .gm-send-btn svg { pointer-events: none; } 148 `; 149 document.head.appendChild(style); 150 151 /* ---- Build HTML ---- */ 152 const wrapper = document.createElement('div'); 153 wrapper.innerHTML = ` 154 <!-- FAB Button --> 155 <button id="gm-chat-fab" aria-label="Open AI Assistant"> 156 <span id="gm-chat-badge">1</span> 157 <svg width="26" height="26" viewBox="0 0 24 24" fill="none"> 158 <path d="M12 2C6.48 2 2 6.48 2 12c0 1.85.5 3.58 1.37 5.07L2 22l4.93-1.37C8.42 21.5 10.15 22 12 22c5.52 0 10-4.48 10-10S17.52 2 12 2z" fill="#0B1F3A"/> 159 <circle cx="8.5" cy="12" r="1.2" fill="#C9A84C"/> 160 <circle cx="12" cy="12" r="1.2" fill="#C9A84C"/> 161 <circle cx="15.5" cy="12" r="1.2" fill="#C9A84C"/> 162 </svg> 163 </button> 164 165 <!-- Chat Window --> 166 <div id="gm-chat-window" class="gm-hidden" role="dialog" aria-label="School AI Assistant"> 167 <div class="gm-chat-header"> 168 <div class="gm-header-avatar">G</div> 169 <div class="gm-header-info"> 170 <div class="gm-header-name">Gadjah Mada Assistant</div> 171 <div class="gm-header-status">Powered by AI · Always here to help</div> 172 </div> 173 <button class="gm-close-btn" id="gm-close-btn" aria-label="Close chat"> 174 <svg width="14" height="14" viewBox="0 0 14 14" fill="currentColor"> 175 <path d="M13 1L1 13M1 1l12 12" stroke="currentColor" stroke-width="2" stroke-linecap="round"/> 176 </svg> 177 </button> 178 </div> 179 180 <div class="gm-messages" id="gm-messages"></div> 181 182 <div class="gm-suggestions" id="gm-suggestions"> 183 <button class="gm-chip">📋 Admissions info</button> 184 <button class="gm-chip">🏆 Achievements</button> 185 <button class="gm-chip">📚 Programs offered</button> 186 <button class="gm-chip">📞 Contact & hours</button> 187 </div> 188 189 <div class="gm-input-row"> 190 <textarea class="gm-input" id="gm-input" placeholder="Ask anything about our school…" rows="1"></textarea> 191 <button class="gm-send-btn" id="gm-send-btn" aria-label="Send message" disabled> 192 <svg width="16" height="16" viewBox="0 0 24 24" fill="none"> 193 <path d="M22 2L11 13M22 2L15 22l-4-9-9-4 20-7z" stroke="#0B1F3A" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"/> 194 </svg> 195 </button> 196 </div> 197 </div> 198 `; 199 document.body.appendChild(wrapper); 200 201 /* ---- State ---- */ 202 const state = { 203 open: false, 204 loading: false, 205 history: [], // { role, content } 206 }; 207 208 /* ---- Elements ---- */ 209 const fab = document.getElementById('gm-chat-fab'); 210 const badge = document.getElementById('gm-chat-badge'); 211 const win = document.getElementById('gm-chat-window'); 212 const closeBtn = document.getElementById('gm-close-btn'); 213 const messages = document.getElementById('gm-messages'); 214 const input = document.getElementById('gm-input'); 215 const sendBtn = document.getElementById('gm-send-btn'); 216 const chips = document.querySelectorAll('.gm-chip'); 217 218 /* ---- Toggle ---- */ 219 function toggleChat() { 220 state.open = !state.open; 221 win.classList.toggle('gm-hidden', !state.open); 222 if (state.open) { 223 badge.style.display = 'none'; 224 if (messages.children.length === 0) addWelcome(); 225 input.focus(); 226 } 227 } 228 fab.addEventListener('click', toggleChat); 229 closeBtn.addEventListener('click', toggleChat); 230 231 /* ---- Welcome ---- */ 232 function addWelcome() { 233 addMessage('bot', 234 '👋 Hello! I\'m the official AI assistant for <strong>Gadjah Mada High School</strong>.\n\nI can help you with admissions, academic programs, activities, faculty, and any other questions about our school.\n\n<em>Feel free to ask me anything!</em>' 235 ); 236 } 237 238 /* ---- Add message to UI ---- */ 239 function addMessage(role, text) { 240 const now = new Date().toLocaleTimeString('id-ID', { hour: '2-digit', minute: '2-digit' }); 241 const div = document.createElement('div'); 242 div.className = `gm-msg gm-${role}`; 243 div.innerHTML = ` 244 <div class="gm-bubble">${text.replace(/\n/g, '<br>')}</div> 245 <span class="gm-time">${now}</span> 246 `; 247 messages.appendChild(div); 248 messages.scrollTop = messages.scrollHeight; 249 return div; 250 } 251 252 /* ---- Typing indicator ---- */ 253 function showTyping() { 254 const div = document.createElement('div'); 255 div.className = 'gm-msg gm-bot gm-typing'; 256 div.id = 'gm-typing'; 257 div.innerHTML = `<div class="gm-bubble"><span class="gm-dot"></span><span class="gm-dot"></span><span class="gm-dot"></span></div>`; 258 messages.appendChild(div); 259 messages.scrollTop = messages.scrollHeight; 260 } 261 function hideTyping() { 262 const t = document.getElementById('gm-typing'); 263 if (t) t.remove(); 264 } 265 266 /* ---- Send message ---- */ 267 async function sendMessage(text) { 268 269 // Remove extra spaces at the beginning and end of the message 270 text = text.trim(); 271 272 // Check two conditions: 273 // 1. If the message is empty 274 // 2. If the chatbot is currently waiting for a response (state.loading is true) 275 // If either condition happens, stop the function using return 276 277 // Hide the suggestion chips that appear before the user sends the first message 278 // Use getElementById to select "gm-suggestions" 279 // Change its display style so it is no longer visible 280 281 // Set the chatbot loading state to true 282 // This prevents users from sending multiple messages while waiting for a reply 283 284 // Disable the send button while the chatbot is processing the request 285 286 // Clear the text inside the input field after the user sends the message 287 288 // Reset the height of the input field back to automatic 289 // This ensures the textarea shrinks back after sending 290 291 // Display the user's message inside the chat interface 292 // Use addMessage() with the role "user" 293 // Make sure the text is passed through escapeHtml() to prevent HTML injection 294 295 // Save the user's message into the conversation history 296 // The history should store: 297 // role: "user" 298 // content: the message text 299 300 // Show the typing indicator so the user knows the bot is generating a reply 301 302 try { 303 304 // Create a constant variable that stores the Gemini API Key 305 const GEMINI_API_KEY = "YOUR_API_KEY_HERE"; 306 307 // Create the Gemini API endpoint URL using the API key above 308 const GEMINI_URL = `https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent?key=${GEMINI_API_KEY}`; 309 310 // Convert the conversation history into the format required by the Gemini API 311 // Each message should contain: 312 // role: either "user" or "model" 313 // parts: an array containing the text message 314 315 // Send a POST request to the Gemini API using fetch() 316 const response = await fetch(GEMINI_URL, { 317 318 // Set the HTTP method to POST 319 320 // Add headers specifying that the content type is JSON 321 322 // The body of the request should be converted into JSON using JSON.stringify() 323 // The request body should contain: 324 // - system_instruction (using SYSTEM_PROMPT) 325 // - contents (the conversation history) 326 // - generationConfig (for example maxOutputTokens) 327 328 }); 329 330 // Check if the API response is not successful 331 // If response.ok is false, throw a new error with the API status code 332 333 // Convert the API response into JSON format 334 const data = 335 336 // Extract the chatbot reply from the returned JSON object 337 // The reply text is usually found inside: 338 // data.candidates[0].content.parts[0].text 339 // If that value does not exist, provide a fallback message like: 340 // "Sorry, I could not answer that right now." 341 const reply = 342 343 // Save the chatbot reply into the conversation history 344 // role should be "assistant" 345 346 // Hide the typing indicator because the response is ready 347 348 // Display the chatbot reply in the chat interface 349 // Use addMessage() with role "bot" 350 // Format the reply using formatReply() 351 352 } catch (err) { 353 354 // If an error occurs, print the error message in the browser console 355 console.error('Chatbot error:', err); 356 357 // Hide the typing indicator so it does not stay on the screen 358 359 // Display an error message inside the chat window 360 // Inform the user that something went wrong 361 // Optionally include a contact number for support 362 363 } finally { 364 365 // Set the loading state back to false 366 // This allows the chatbot to accept new messages 367 368 // Re-enable the send button 369 // The button should only be enabled if the input field contains text 370 } 371 } 372 373 /* ---- Format reply (basic markdown-ish) ---- */ 374 function formatReply(text) { 375 return escapeHtml(text) 376 .replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>') 377 .replace(/\*(.*?)\*/g, '<em>$1</em>') 378 .replace(/\n/g, '<br>'); 379 } 380 381 function escapeHtml(str) { 382 return str.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>'); 383 } 384 385 /* ---- Input events ---- */ 386 input.addEventListener('input', () => { 387 sendBtn.disabled = !input.value.trim() || state.loading; 388 // Auto-grow textarea 389 input.style.height = 'auto'; 390 input.style.height = Math.min(input.scrollHeight, 90) + 'px'; 391 }); 392 393 input.addEventListener('keydown', (e) => { 394 if (e.key === 'Enter' && !e.shiftKey) { 395 e.preventDefault(); 396 if (!sendBtn.disabled) sendMessage(input.value); 397 } 398 }); 399 400 sendBtn.addEventListener('click', () => sendMessage(input.value)); 401 402 /* ---- Chips ---- */ 403 chips.forEach(chip => { 404 chip.addEventListener('click', () => { 405 const map = { 406 '📋 Admissions info': 'How do I apply to Gadjah Mada High School? What are the requirements?', 407 '🏆 Achievements': 'What are the school\'s latest awards and achievements?', 408 '📚 Programs offered': 'What academic programs are available at this school?', 409 '📞 Contact & hours': 'What are the school\'s contact details and office hours?', 410 }; 411 const q = map[chip.textContent.trim()] || chip.textContent; 412 input.value = q; 413 sendMessage(q); 414 }); 415 }); 416 417 })();