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 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 // ====================================== 2 // API KEY FOR GOOGLE GEMINI 3 // Get your own from: https://aistudio.google.com/app/apikey 4 // ====================================== 5 const GEMINI_API_KEY = "PUT_YOUR_API_KEY_HERE"; 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 // GENERATE REFLECTION - Main function (STUDENT TASK) 107 // ====================================== 108 function generateReflection() { 109 110 // Get the text from the input field (id: "habitInput") 111 // Use .value to access the text and .trim() to remove extra spaces 112 const userText = 113 114 // Check if the input is empty 115 // If the text is empty: 116 // - Call shakeForm() to show an error animation 117 // - Stop the function using return 118 119 120 // Get the current time using Date.now() 121 // This will be used to control how often the user can send requests 122 const currentTime = 123 124 // Calculate how much time has passed since the last request 125 // Subtract lastRequestTime from currentTime 126 const timePassed = 127 128 // Check if the user is sending requests too quickly 129 // If timePassed is less than MIN_REQUEST_INTERVAL: 130 // - Calculate how many seconds the user still needs to wait 131 // - Show an alert message telling the user to wait 132 // - Stop the function using return 133 134 135 // Save the current time into lastRequestTime 136 // This marks the time of the latest request 137 138 139 // Turn on the loading state for the button 140 // Call setBtnLoading(true) 141 142 143 // Show the loading animation in the result area 144 // Call showLoading() 145 146 147 // Create the message that will be sent to the Gemini API 148 // The message should: 149 // - Act as a lifestyle coach 150 // - Include the user's input (userText) 151 // - Ask for: 152 // 1 reflection question 153 // 1 improvement suggestion 154 // - Request the response in JSON format 155 const message = 156 157 158 // Send the message to Gemini API 159 // Call sendToGemini() and pass the message as the parameter 160 161 } 162 163 164 // ====================================== 165 // SEND TO GEMINI - Make API request 166 // ====================================== 167 function sendToGemini(message) { 168 // Prepare the request data 169 const requestData = { 170 contents: [ 171 { 172 parts: [ 173 { 174 text: message 175 } 176 ] 177 } 178 ] 179 }; 180 181 // Make the API request 182 fetch( 183 "https://generativelanguage.googleapis.com/v1/models/gemini-2.5-flash:generateContent?key=" + GEMINI_API_KEY, 184 { 185 method: "POST", 186 headers: { 187 "Content-Type": "application/json" 188 }, 189 body: JSON.stringify(requestData) 190 } 191 ) 192 .then(function(response) { 193 // Check if response is OK 194 if (response.ok) { 195 return response.json(); 196 } else { 197 throw new Error('API returned status ' + response.status); 198 } 199 }) 200 .then(function(data) { 201 // Get the text response from Gemini 202 let textResponse = data.candidates[0].content.parts[0].text; 203 204 // FIXED: Clean the text response to remove markdown backticks (e.g. ```json ... ```) 205 // This fixes the "Unexpected token" JSON error 206 const cleanedJson = textResponse.replace(/```json/g, '').replace(/```/g, '').trim(); 207 208 // Try to parse the JSON 209 const result = JSON.parse(cleanedJson); 210 211 // Extract question - try different property names 212 let question = result.question || result.reflectionQuestion || result.reflection_question || 'No question provided'; 213 214 // Extract suggestion - try different property names 215 let suggestion = result.suggestion || result.improvementSuggestion || result.improvement_suggestion || 'No suggestion provided'; 216 217 // Check if we got undefined or empty values 218 if (!question || question === 'undefined') { 219 throw new Error('Question is missing or undefined in API response'); 220 } 221 222 if (!suggestion || suggestion === 'undefined') { 223 throw new Error('Suggestion is missing or undefined in API response'); 224 } 225 226 // Show the results 227 showResult(question, suggestion); 228 }) 229 .catch(function(error) { 230 // Show error message 231 showError('Error: ' + error.message); 232 }) 233 .finally(function() { 234 // Reset button 235 setBtnLoading(false); 236 }); 237 } 238 239 240 241 242 // ====================================== 243 // SET BUTTON LOADING - Show/hide loading state on button 244 // ====================================== 245 function setBtnLoading(isLoading) { 246 const button = document.getElementById('generateBtn'); 247 248 if (isLoading) { 249 button.disabled = true; 250 button.innerHTML = '<span>Thinking…</span>'; 251 } else { 252 button.disabled = false; 253 button.innerHTML = '<i class="bi bi-stars me-2"></i>Generate Reflection'; 254 } 255 } 256 257 258 // ====================================== 259 // SHOW LOADING - Display loading animation 260 // ====================================== 261 function showLoading() { 262 const resultArea = document.getElementById('result-area'); 263 264 resultArea.style.display = 'block'; 265 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>'; 266 267 // Scroll to show the loading message 268 resultArea.scrollIntoView({ behavior: 'smooth', block: 'nearest' }); 269 } 270 271 272 // ====================================== 273 // SHOW RESULT - Display the AI response 274 // ====================================== 275 function showResult(question, suggestion) { 276 const resultArea = document.getElementById('result-area'); 277 278 // Create the HTML to show 279 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>' + 280 '<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>' + 281 '<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>' + 282 '<div class="try-again-row"><button class="btn-try-again" onclick="resetAll()"><i class="bi bi-arrow-counterclockwise"></i> Try another habit</button></div>'; 283 284 resultArea.style.display = 'block'; 285 resultArea.innerHTML = html; 286 287 // Scroll to show the result 288 resultArea.scrollIntoView({ behavior: 'smooth', block: 'nearest' }); 289 } 290 291 292 // ====================================== 293 // SHOW ERROR - Display error message 294 // ====================================== 295 function showError(errorMessage) { 296 const resultArea = document.getElementById('result-area'); 297 298 const html = '<div class="error-state"><i class="bi bi-exclamation-triangle-fill"></i><span>' + errorMessage + '</span></div>'; 299 300 resultArea.style.display = 'block'; 301 resultArea.innerHTML = html; 302 } 303 304 305 // ====================================== 306 // RESET ALL - Clear everything and start fresh 307 // ====================================== 308 function resetAll() { 309 // Clear input field 310 clearInput(); 311 312 // Hide results 313 const resultArea = document.getElementById('result-area'); 314 resultArea.style.display = 'none'; 315 resultArea.innerHTML = ''; 316 317 // Scroll back to top 318 window.scrollTo({ top: 0, behavior: 'smooth' }); 319 } 320 321 322 // ====================================== 323 // SHAKE FORM - Animated shake when input is empty 324 // ====================================== 325 function shakeForm() { 326 // Get the form element 327 const form = document.querySelector('.form-surface'); 328 329 // Reset animation 330 form.style.animation = 'none'; 331 form.style.transform = 'translateX(0)'; 332 333 // Variables for shake animation 334 let shakeCount = 0; 335 let shakeDirection = 1; 336 337 // Create shake effect 338 const shakeInterval = setInterval(function() { 339 form.style.transform = 'translateX(' + (shakeDirection * 5) + 'px)'; 340 shakeDirection = shakeDirection * -1; 341 shakeCount = shakeCount + 1; 342 343 // Stop after 8 shakes 344 if (shakeCount > 7) { 345 clearInterval(shakeInterval); 346 form.style.transform = 'none'; 347 } 348 }, 55); 349 350 // Get the input field 351 const inputField = document.getElementById('habitInput'); 352 353 // Turn the border red 354 inputField.style.borderBottomColor = '#ff6464'; 355 356 // Turn it back to normal after 1.2 seconds 357 setTimeout(function() { 358 inputField.style.borderBottomColor = ''; 359 }, 1200); 360 361 // Focus on the input 362 inputField.focus(); 363 }