challenge13-webdev

public
tiara Mar 17, 2026 Never 81
Clone
CSS challenge13-webdev-css 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
}
HTML challenge13-webdev-html 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>
JavaScript challenge13-webdev-js 326 lines (258 loc) | 9.98 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 = "";
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
}
JavaScript challenge14-webdev-chatbot 417 lines (351 loc) | 20 KB
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,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');
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
})();