challenge13-webdev

public
tiara Apr 06, 2026 Never 51
Clone
HTML index
CSS style
JavaScript script
HTML index 93 lines (79 loc) | 3.88 KB
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 &nbsp;·&nbsp;
86
Powered by <a href="https://deepmind.google/technologies/gemini/" target="_blank">Google Gemini</a>
87
&nbsp;·&nbsp; 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>
CSS style 494 lines (434 loc) | 13.36 KB
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
}
JavaScript script 363 lines (282 loc) | 11.44 KB
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
}