1 :root { 2 --bg: #f7f4f0; 3 --surface: #fdfcfa; 4 --surface2: #f2ede8; 5 --border: #e8e1d9; 6 --border-focus:#b8a99a; 7 8 --text: #2d2620; 9 --text-2: #6b5f56; 10 --text-3: #a8998e; 11 12 --sage: #8aaa8c; 13 --sage-light: #edf2ed; 14 --sage-mid: #c5d9c6; 15 16 --blush: #c9918a; 17 --blush-light: #f5ebe9; 18 --blush-mid: #e4b8b3; 19 20 --sky: #88a8c0; 21 --sky-light: #e9f0f5; 22 --sky-mid: #b8d0e0; 23 24 --sand: #c4a882; 25 --sand-light: #f5ede0; 26 --sand-mid: #dfc9a8; 27 28 --lavender: #9e92c0; 29 --lavender-light: #eeecf5; 30 --lavender-mid: #ccc5e2; 31 32 --radius: 14px; 33 --radius-lg: 20px; 34 } 35 36 *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; } 37 38 body { 39 background: var(--bg); 40 color: var(--text); 41 font-family: 'DM Sans', sans-serif; 42 min-height: 100vh; 43 } 44 45 body::before { 46 content: ''; 47 position: fixed; inset: 0; pointer-events: none; z-index: 0; 48 background: 49 radial-gradient(ellipse 55% 45% at 5% 0%, rgba(138,170,140,.13) 0%, transparent 60%), 50 radial-gradient(ellipse 45% 40% at 95% 100%, rgba(136,168,192,.10) 0%, transparent 60%), 51 radial-gradient(ellipse 35% 30% at 60% 40%, rgba(201,145,138,.06) 0%, transparent 60%); 52 } 53 54 /* ── HEADER ── */ 55 .site-header { 56 position: sticky; top: 0; z-index: 100; 57 background: rgba(247,244,240,.88); 58 backdrop-filter: blur(20px); 59 -webkit-backdrop-filter: blur(20px); 60 border-bottom: 1px solid var(--border); 61 height: 62px; 62 display: flex; align-items: center; justify-content: space-between; 63 padding: 0 40px; 64 } 65 66 .brand { display: flex; align-items: center; gap: 11px; text-decoration: none; } 67 .brand-mark { 68 width: 36px; height: 36px; border-radius: 12px; 69 background: linear-gradient(135deg, var(--sage-mid), var(--sky-mid)); 70 display: flex; align-items: center; justify-content: center; 71 font-size: 16px; 72 } 73 .brand-name { 74 font-family: 'Fraunces', serif; 75 font-size: 22px; font-weight: 600; letter-spacing: -.3px; 76 color: var(--text); 77 } 78 .brand-name span { color: var(--sage); } 79 80 .hdr-date { 81 font-family: 'DM Mono', monospace; 82 font-size: 11px; color: var(--text-3); 83 background: var(--surface2); 84 border: 1px solid var(--border); 85 border-radius: 20px; padding: 4px 12px; 86 } 87 88 /* ── LAYOUT ── */ 89 .wrap { 90 position: relative; z-index: 1; 91 max-width: 1260px; margin: 0 auto; 92 padding: 40px 40px 72px; 93 } 94 95 /* ── STEP LABELS ── */ 96 .step-lbl { 97 font-family: 'DM Mono', monospace; 98 font-size: 10px; letter-spacing: 2px; text-transform: uppercase; 99 color: var(--text-3); margin-bottom: 5px; 100 display: flex; align-items: center; gap: 7px; 101 } 102 .step-lbl::before { 103 content: ''; width: 16px; height: 1px; 104 background: var(--text-3); 105 } 106 .sec-title { 107 font-family: 'Fraunces', serif; 108 font-size: 26px; font-weight: 600; letter-spacing: -.4px; 109 color: var(--text); margin-bottom: 22px; 110 line-height: 1.2; 111 } 112 .sec-title em { font-style: italic; font-weight: 300; color: var(--text-2); } 113 114 /* ── FORM CARD ── */ 115 .form-card { 116 background: var(--surface); 117 border: 1px solid var(--border); 118 border-radius: var(--radius-lg); 119 padding: 28px; 120 box-shadow: 0 1px 3px rgba(45,38,32,.04), 0 4px 16px rgba(45,38,32,.04); 121 } 122 123 /* ── INPUTS ── */ 124 .f-label { 125 display: block; font-size: 11.5px; font-weight: 500; 126 color: var(--text-2); margin-bottom: 6px; letter-spacing: .2px; 127 } 128 .opt-tag { 129 font-size: 10px; color: var(--text-3); font-weight: 400; margin-left: 4px; 130 } 131 132 .f-input, .f-select, .f-textarea { 133 width: 100%; 134 background: var(--surface2); 135 border: 1.5px solid var(--border); 136 border-radius: 10px; 137 padding: 10px 14px; 138 color: var(--text); 139 font-family: 'DM Sans', sans-serif; 140 font-size: 13.5px; 141 outline: none; 142 transition: border-color .2s, box-shadow .2s, background .2s; 143 } 144 .f-input::placeholder, .f-textarea::placeholder { color: var(--text-3); } 145 .f-input:focus, .f-select:focus, .f-textarea:focus { 146 border-color: var(--border-focus); 147 background: var(--surface); 148 box-shadow: 0 0 0 3px rgba(138,170,140,.15); 149 } 150 .f-input[type="date"]::-webkit-calendar-picker-indicator { 151 opacity: .5; cursor: pointer; 152 } 153 .f-select { 154 cursor: pointer; 155 background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='8'%3E%3Cpath d='M1 1l5 5 5-5' stroke='%23a8998e' stroke-width='1.5' fill='none' stroke-linecap='round'/%3E%3C/svg%3E"); 156 background-repeat: no-repeat; 157 background-position: right 14px center; 158 padding-right: 38px; 159 } 160 .f-select option { background: var(--surface); color: var(--text); } 161 .f-textarea { resize: vertical; min-height: 76px; line-height: 1.65; } 162 163 /* ── PILL TOGGLE ── */ 164 .pill-grp { 165 display: flex; gap: 6px; 166 } 167 .pill-opt input { display: none; } 168 .pill-opt label { 169 display: block; padding: 8px 18px; 170 background: var(--surface); border: 1.5px solid var(--border); 171 border-radius: 40px; font-size: 12.5px; font-weight: 500; 172 cursor: pointer; color: var(--text-3); 173 transition: all .18s; white-space: nowrap; 174 user-select: none; 175 } 176 .pill-opt label:hover { border-color: var(--border-focus); color: var(--text-2); background: var(--surface2); } 177 .pill-opt.sage input:checked + label { 178 background: var(--sage); border-color: var(--sage); 179 color: #fff; font-weight: 600; 180 box-shadow: 0 2px 10px rgba(138,170,140,.35); 181 } 182 .pill-opt.blush input:checked + label { 183 background: var(--blush); border-color: var(--blush); 184 color: #fff; font-weight: 600; 185 box-shadow: 0 2px 10px rgba(201,145,138,.35); 186 } 187 .pill-opt.sky input:checked + label { 188 background: var(--sky); border-color: var(--sky); 189 color: #fff; font-weight: 600; 190 box-shadow: 0 2px 10px rgba(136,168,192,.35); 191 } 192 .pill-opt.sand input:checked + label { 193 background: var(--sand); border-color: var(--sand); 194 color: #fff; font-weight: 600; 195 box-shadow: 0 2px 10px rgba(196,168,130,.35); 196 } 197 .pill-opt.lav input:checked + label { 198 background: var(--lavender); border-color: var(--lavender); 199 color: #fff; font-weight: 600; 200 box-shadow: 0 2px 10px rgba(158,146,192,.35); 201 } 202 .pill-opt input:checked + label { 203 background: var(--sage); border-color: var(--sage); 204 color: #fff; font-weight: 600; 205 } 206 207 /* ── ADD BTN ── */ 208 .add-btn { 209 width: 100%; padding: 11px 16px; 210 background: transparent; 211 border: 1.5px dashed var(--border); 212 border-radius: 10px; 213 color: var(--text-3); font-family: 'DM Sans', sans-serif; 214 font-size: 13px; font-weight: 500; 215 cursor: pointer; 216 display: flex; align-items: center; justify-content: center; gap: 8px; 217 transition: all .2s; 218 } 219 .add-btn:hover { 220 border-color: var(--sage); color: var(--sage); 221 background: var(--sage-light); 222 } 223 224 /* ── DIVIDER ── */ 225 .soft-divider { 226 height: 1px; 227 background: linear-gradient(90deg, transparent, var(--border), transparent); 228 margin: 22px 0; 229 } 230 231 /* ── TASK LIST ── */ 232 .task-list { display: flex; flex-direction: column; gap: 7px; } 233 234 .task-card { 235 background: var(--surface); 236 border: 1px solid var(--border); 237 border-radius: 12px; 238 padding: 13px 16px; 239 display: flex; align-items: flex-start; gap: 11px; 240 animation: slideDown .22s ease; 241 transition: border-color .2s, box-shadow .2s; 242 } 243 .task-card:hover { 244 border-color: var(--border-focus); 245 box-shadow: 0 2px 12px rgba(45,38,32,.06); 246 } 247 @keyframes slideDown { from { opacity:0; transform:translateY(-8px); } to { opacity:1; transform:translateY(0); } } 248 249 .task-idx { 250 font-family: 'DM Mono', monospace; 251 font-size: 11px; color: var(--text-3); 252 min-width: 18px; padding-top: 2px; 253 } 254 .task-body { flex: 1; min-width: 0; } 255 .task-name-text { 256 font-size: 13.5px; font-weight: 500; 257 margin-bottom: 7px; 258 white-space: nowrap; overflow: hidden; text-overflow: ellipsis; 259 } 260 .task-chips { display: flex; flex-wrap: wrap; gap: 5px; } 261 262 .chip { 263 display: inline-flex; align-items: center; gap: 4px; 264 font-size: 11px; padding: 3px 9px; border-radius: 20px; 265 border: 1px solid; font-weight: 500; font-family: 'DM Sans', sans-serif; 266 } 267 .chip-deadline { color: var(--sand); background: var(--sand-light); border-color: var(--sand-mid); } 268 .chip-overdue { color: var(--blush); background: var(--blush-light); border-color: var(--blush-mid); } 269 .chip-type { color: var(--sky); background: var(--sky-light); border-color: var(--sky-mid); } 270 .chip-solo { color: var(--sage); background: var(--sage-light); border-color: var(--sage-mid); } 271 .chip-group { color: var(--lavender); background: var(--lavender-light); border-color: var(--lavender-mid); } 272 .chip-easy { color: var(--sage); background: var(--sage-light); border-color: var(--sage-mid); } 273 .chip-medium { color: var(--sand); background: var(--sand-light); border-color: var(--sand-mid); } 274 .chip-hard { color: var(--blush); background: var(--blush-light); border-color: var(--blush-mid); } 275 .chip-subject { color: var(--text-3); background: var(--surface2); border-color: var(--border); } 276 277 .task-del { 278 background: none; border: none; cursor: pointer; 279 color: var(--text-3); font-size: 13px; padding: 2px; flex-shrink: 0; 280 transition: color .15s; 281 } 282 .task-del:hover { color: var(--blush); } 283 284 /* EMPTY */ 285 .empty-st { 286 text-align: center; padding: 30px 20px; 287 color: var(--text-3); font-size: 13px; line-height: 2.1; 288 } 289 .empty-icon { font-size: 28px; margin-bottom: 6px; opacity: .5; } 290 291 /* ── ANALYZE BUTTON ── */ 292 .analyze-btn { 293 width: 100%; padding: 14px 20px; 294 background: var(--text); 295 border: none; border-radius: 14px; 296 color: var(--surface); 297 font-family: 'Fraunces', serif; 298 font-size: 16px; font-weight: 400; letter-spacing: .1px; 299 cursor: pointer; 300 display: flex; align-items: center; justify-content: center; gap: 10px; 301 transition: all .22s; 302 box-shadow: 0 2px 12px rgba(45,38,32,.12); 303 } 304 .analyze-btn:hover:not(:disabled) { 305 background: #3d342c; 306 transform: translateY(-1px); 307 box-shadow: 0 4px 20px rgba(45,38,32,.18); 308 } 309 .analyze-btn:disabled { 310 background: var(--surface2); color: var(--text-3); 311 box-shadow: none; cursor: not-allowed; 312 } 313 314 /* ── RESULT PANEL ── */ 315 .result-sticky { position: sticky; top: 82px; } 316 317 .result-card { 318 background: var(--surface); 319 border: 1px solid var(--border); 320 border-radius: var(--radius-lg); 321 min-height: 440px; overflow: hidden; 322 box-shadow: 0 1px 3px rgba(45,38,32,.04), 0 4px 16px rgba(45,38,32,.04); 323 } 324 325 /* Placeholder */ 326 .r-placeholder { 327 display: flex; flex-direction: column; 328 align-items: center; justify-content: center; 329 padding: 64px 40px; text-align: center; gap: 14px; 330 min-height: 440px; 331 } 332 .r-glyph { 333 width: 68px; height: 68px; border-radius: 50%; 334 background: var(--sage-light); 335 border: 1.5px solid var(--sage-mid); 336 display: flex; align-items: center; justify-content: center; 337 font-size: 26px; 338 } 339 .r-title { font-family: 'Fraunces', serif; font-size: 19px; font-weight: 600; color: var(--text); } 340 .r-sub { font-size: 13px; color: var(--text-3); line-height: 1.75; max-width: 240px; } 341 342 /* Loading */ 343 .loading-wrap { 344 display: none; flex-direction: column; 345 align-items: center; justify-content: center; 346 gap: 16px; padding: 64px 40px; text-align: center; 347 min-height: 440px; 348 } 349 .loading-wrap.show { display: flex; } 350 .spinner { 351 width: 40px; height: 40px; 352 border: 2px solid var(--border); 353 border-top-color: var(--sage); 354 border-right-color: var(--sky); 355 border-radius: 50%; 356 animation: spin .9s linear infinite; 357 } 358 @keyframes spin { to { transform: rotate(360deg); } } 359 .loading-title { font-family: 'Fraunces', serif; font-size: 17px; font-weight: 600; color: var(--text); } 360 .loading-sub { font-size: 11.5px; color: var(--text-3); font-family: 'DM Mono', monospace; letter-spacing: .5px; } 361 .dots span { 362 display: inline-block; width: 6px; height: 6px; border-radius: 50%; 363 background: var(--sage-mid); margin: 0 2px; 364 animation: blink 1.4s ease-in-out infinite both; 365 } 366 .dots span:nth-child(2) { animation-delay: .2s; } 367 .dots span:nth-child(3) { animation-delay: .4s; } 368 @keyframes blink { 0%,80%,100%{opacity:.2;transform:scale(.8)} 40%{opacity:1;transform:scale(1)} } 369 370 /* Result body */ 371 .result-body { display: none; } 372 .result-body.show { display: block; } 373 374 .r-topbar { 375 padding: 18px 24px; border-bottom: 1px solid var(--border); 376 display: flex; align-items: center; justify-content: space-between; 377 } 378 .r-topbar-title { font-family: 'Fraunces', serif; font-size: 17px; font-weight: 600; } 379 .r-badge { 380 font-family: 'DM Mono', monospace; font-size: 10px; color: var(--text-3); 381 background: var(--surface2); border: 1px solid var(--border); 382 border-radius: 20px; padding: 3px 10px; 383 } 384 385 /* Strategy box */ 386 .strategy-box { 387 margin: 18px 22px; 388 padding: 16px 18px; 389 background: var(--sage-light); 390 border: 1px solid var(--sage-mid); 391 border-radius: 14px; 392 border-left: 3px solid var(--sage); 393 } 394 .strategy-lbl { 395 font-size: 10px; font-weight: 600; letter-spacing: 1.5px; 396 text-transform: uppercase; color: var(--sage); margin-bottom: 6px; 397 font-family: 'DM Mono', monospace; 398 } 399 .strategy-text { font-size: 13px; line-height: 1.75; color: var(--text-2); } 400 401 /* Priority items */ 402 .p-item { 403 padding: 20px 22px; border-bottom: 1px solid var(--border); 404 animation: fadeRise .38s ease both; 405 } 406 .p-item:last-child { border-bottom: none; } 407 @keyframes fadeRise { from { opacity:0; transform:translateY(12px); } to { opacity:1; transform:translateY(0); } } 408 409 .p-top { display: flex; align-items: flex-start; gap: 14px; margin-bottom: 12px; } 410 411 .rank-badge { 412 min-width: 42px; height: 42px; border-radius: 14px; 413 display: flex; align-items: center; justify-content: center; 414 font-family: 'Fraunces', serif; 415 font-size: 22px; font-weight: 600; flex-shrink: 0; 416 } 417 .rk-1 { background: var(--blush-light); color: var(--blush); border: 1.5px solid var(--blush-mid); } 418 .rk-2 { background: var(--sand-light); color: var(--sand); border: 1.5px solid var(--sand-mid); } 419 .rk-3 { background: var(--sky-light); color: var(--sky); border: 1.5px solid var(--sky-mid); } 420 .rk-n { background: var(--surface2); color: var(--text-3); border: 1.5px solid var(--border); } 421 422 .p-name { font-size: 14.5px; font-weight: 600; margin-bottom: 7px; color: var(--text); } 423 .p-chips { display: flex; flex-wrap: wrap; gap: 5px; } 424 425 /* Urgency bar */ 426 .urg-row { display: flex; align-items: center; gap: 10px; margin-bottom: 12px; } 427 .urg-lbl { 428 font-size: 11px; color: var(--text-3); min-width: 54px; 429 font-family: 'DM Mono', monospace; 430 } 431 .urg-track { 432 flex: 1; height: 5px; 433 background: var(--surface2); border-radius: 10px; overflow: hidden; 434 } 435 .urg-fill { height: 100%; border-radius: 10px; transition: width 1.2s cubic-bezier(.16,1,.3,1); } 436 .urg-fill.critical { background: var(--blush-mid); } 437 .urg-fill.high { background: var(--sand-mid); } 438 .urg-fill.medium { background: var(--sky-mid); } 439 .urg-fill.low { background: var(--sage-mid); } 440 .urg-score { 441 font-family: 'DM Mono', monospace; font-size: 11px; 442 color: var(--text-3); min-width: 26px; text-align: right; 443 } 444 445 /* Reason box */ 446 .reason-box { 447 background: var(--surface2); border: 1px solid var(--border); 448 border-left: 2.5px solid var(--lavender-mid); 449 border-radius: 0 10px 10px 0; 450 padding: 12px 14px; 451 } 452 .reason-head { 453 font-size: 10px; font-weight: 600; letter-spacing: 1.5px; 454 text-transform: uppercase; color: var(--lavender); margin-bottom: 5px; 455 font-family: 'DM Mono', monospace; 456 } 457 .reason-text { font-size: 12.5px; line-height: 1.75; color: var(--text-2); } 458 .tip-row { 459 margin-top: 9px; padding-top: 9px; 460 border-top: 1px solid var(--border); 461 font-size: 12px; color: var(--sand); 462 display: flex; gap: 7px; align-items: flex-start; 463 } 464 465 /* SCROLLBAR */ 466 ::-webkit-scrollbar { width: 4px; } 467 ::-webkit-scrollbar-track { background: transparent; } 468 ::-webkit-scrollbar-thumb { background: var(--border); border-radius: 2px; } 469 470 @media (max-width: 920px) { 471 .main-grid { flex-direction: column; } 472 .result-sticky { position: static; } 473 .site-header, .wrap { padding-left: 18px; padding-right: 18px; } 474 .wrap { padding-top: 24px; } 475 }
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>Taskly — AI Priority Planner</title> 7 <link href="https://fonts.googleapis.com/css2?family=DM+Sans:ital,wght@0,300;0,400;0,500;0,600;1,300;1,400&family=Fraunces:ital,wght@0,300;0,400;0,600;1,300;1,400&family=DM+Mono:wght@300;400&display=swap" rel="stylesheet"> 8 <link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" rel="stylesheet"> 9 <link rel="stylesheet" href="style.css"> 10 </head> 11 <body> 12 <!-- HEADER --> 13 <header class="site-header"> 14 <a class="brand" href="#"> 15 <div class="brand-mark">✦</div> 16 <span class="brand-name">Task<span>ly</span></span> 17 </a> 18 <span class="hdr-date" id="headerDate"></span> 19 </header> 20 21 <div id="corsBanner" style="display:none;background:#fef3dc;border-bottom:1px solid #dfc9a8;padding:10px 40px;font-size:12.5px;color:#7a5c2a;font-family:'DM Sans',sans-serif;text-align:center"> 22 <strong>Open this file via a local server</strong> for the AI to work. 23 In your terminal: <code style="background:rgba(0,0,0,.07);padding:2px 7px;border-radius:4px">npx serve .</code> — then open the printed URL in your browser. 24 </div> 25 <script> 26 if (location.protocol === 'file:') document.getElementById('corsBanner').style.display = 'block'; 27 </script> 28 29 30 <div class="wrap"> 31 <div class="row g-4" style="--bs-gutter-x:32px"> 32 33 <!-- LEFT --> 34 <div class="col-12 col-lg-5"> 35 <div class="step-lbl">Step 01</div> 36 <div class="sec-title">Add this week's <em>tasks</em></div> 37 38 <div class="form-card mb-3"> 39 <div class="mb-3"> 40 <label class="f-label">Task Name <span style="color:var(--blush)">*</span></label> 41 <input class="f-input" id="taskName" placeholder="e.g. Organic Chemistry Lab Report" autocomplete="off"/> 42 </div> 43 44 <div class="row g-3 mb-3"> 45 <div class="col-7"> 46 <label class="f-label">Deadline <span style="color:var(--blush)">*</span></label> 47 <input class="f-input" id="taskDeadline" type="date"/> 48 </div> 49 <div class="col-5"> 50 <label class="f-label">Est. Hours <span class="opt-tag">(optional)</span></label> 51 <input class="f-input" id="taskHours" type="number" min="0.5" max="99" step="0.5" placeholder="3"/> 52 </div> 53 </div> 54 55 <div class="row g-3 mb-3"> 56 <div class="col-6"> 57 <label class="f-label">Task Type</label> 58 <select class="f-select" id="taskType"> 59 <option value="">— Select —</option> 60 <option>Lab Report</option> 61 <option>Essay / Paper</option> 62 <option>Presentation</option> 63 <option>Quiz / Exam</option> 64 <option>Coding / Project</option> 65 <option>Practicum</option> 66 <option>Research</option> 67 <option>Routine Assignment</option> 68 <option>Other</option> 69 </select> 70 </div> 71 <div class="col-6"> 72 <label class="f-label">Subject / Course</label> 73 <input class="f-input" id="taskSubject" placeholder="e.g. Organic Chem"/> 74 </div> 75 </div> 76 77 <div class="mb-3"> 78 <label class="f-label">Difficulty</label> 79 <div class="pill-grp"> 80 <div class="pill-opt sage"><input type="radio" name="diff" id="d1" value="Easy"><label for="d1">Easy</label></div> 81 <div class="pill-opt sand"><input type="radio" name="diff" id="d2" value="Medium" checked><label for="d2">Medium</label></div> 82 <div class="pill-opt blush"><input type="radio" name="diff" id="d3" value="Hard"><label for="d3">Hard</label></div> 83 </div> 84 </div> 85 86 <div class="mb-3"> 87 <label class="f-label">Work Type</label> 88 <div class="pill-grp"> 89 <div class="pill-opt sky"><input type="radio" name="ptype" id="p1" value="Individual" checked><label for="p1">👤 Individual</label></div> 90 <div class="pill-opt lav"><input type="radio" name="ptype" id="p2" value="Group"><label for="p2">👥 Group</label></div> 91 </div> 92 </div> 93 94 <div class="mb-3"> 95 <label class="f-label">Grade Weight</label> 96 <div class="pill-grp flex-wrap" style="gap:6px"> 97 <div class="pill-opt sage"><input type="radio" name="weight" id="w1" value="Low"><label for="w1">Low</label></div> 98 <div class="pill-opt sand"><input type="radio" name="weight" id="w2" value="Medium" checked><label for="w2">Medium</label></div> 99 <div class="pill-opt sky"><input type="radio" name="weight" id="w3" value="High"><label for="w3">High</label></div> 100 <div class="pill-opt blush"><input type="radio" name="weight" id="w4" value="Final"><label for="w4">Final / Exam</label></div> 101 </div> 102 </div> 103 104 <div class="mb-3"> 105 <label class="f-label">Notes <span class="opt-tag">(optional)</span></label> 106 <textarea class="f-textarea" id="taskNotes" placeholder="e.g. Need lab access, coordinate with teammates first..."></textarea> 107 </div> 108 109 <button class="add-btn" onclick="addTask()"> 110 <svg width="15" height="15" viewBox="0 0 15 15" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"><circle cx="7.5" cy="7.5" r="6.5"/><line x1="7.5" y1="4.5" x2="7.5" y2="10.5"/><line x1="4.5" y1="7.5" x2="10.5" y2="7.5"/></svg> Add to List 111 </button> 112 </div> 113 114 <!-- TASK LIST --> 115 <div class="d-flex align-items-center justify-content-between mb-2 px-1"> 116 <div class="step-lbl mb-0">Step 02</div> 117 <span id="taskCount" style="font-family:'DM Mono',monospace;font-size:11px;color:var(--text-3)">0 tasks</span> 118 </div> 119 120 <div class="task-list mb-3" id="taskList"> 121 <div class="empty-st"> 122 <div class="empty-icon">🗒️</div> 123 No tasks yet.<br>Fill out the form above and click <strong style="color:var(--text)">"Add to List"</strong> 124 </div> 125 </div> 126 127 <button class="analyze-btn" id="analyzeBtn" onclick="analyzeTask()" disabled> 128 <svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor"><path d="M8 1l1.5 3.5L13 6l-2.5 2.5.6 3.5L8 10.4l-3.1 1.6.6-3.5L3 6l3.5-.5L8 1z"/></svg> Analyze & Prioritize 129 </button> 130 </div> 131 132 <!-- RIGHT --> 133 <div class="col-12 col-lg-7"> 134 <div class="result-sticky"> 135 <div class="step-lbl">Step 03</div> 136 <div class="sec-title" style="color:var(--sage);">AI <em>Recommendations</em></div> 137 138 <div class="result-card"> 139 140 <div class="r-placeholder" id="placeholder"> 141 <div class="r-glyph">🌿</div> 142 <div class="r-title">Waiting for your tasks</div> 143 <div class="r-sub">Add at least one task, then click <strong>Analyze & Prioritize</strong> to get a calm, clear plan.</div> 144 </div> 145 146 <div class="loading-wrap" id="loadingWrap"> 147 <div class="spinner"></div> 148 <div class="loading-title">Thinking it through…</div> 149 <div class="loading-sub">deadlines · difficulty · workload</div> 150 <div class="dots"><span></span><span></span><span></span></div> 151 </div> 152 153 <div class="result-body" id="resultBody"> 154 <div class="r-topbar"> 155 <div class="r-topbar-title">Priority Order</div> 156 <div class="r-badge" id="resultMeta"></div> 157 </div> 158 <div id="strategyWrap"></div> 159 <div id="priorityList"></div> 160 </div> 161 162 </div> 163 </div> 164 </div> 165 166 </div> 167 </div> 168 169 <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.bundle.min.js"></script> 170 <script src="script.js"></script> 171 </body> 172 </html>
1 // Initialize page 2 let tasks = []; 3 const today = new Date().toISOString().split('T')[0]; 4 5 document.getElementById('headerDate').textContent = new Date().toLocaleDateString('en-US', {weekday:'short', month:'short', day:'numeric', year:'numeric'}); 6 document.getElementById('taskDeadline').min = today; 7 8 // Get radio button value 9 function getRadioValue(radioName) { 10 const element = document.querySelector('input[name="' + radioName + '"]:checked'); 11 if (element) { 12 return element.value; 13 } 14 return ''; 15 } 16 17 // Show error highlight on input 18 function showError(elementId) { 19 const element = document.getElementById(elementId); 20 element.style.borderColor = 'var(--blush)'; 21 element.style.boxShadow = '0 0 0 3px rgba(201,145,138,.18)'; 22 23 setTimeout(function() { 24 element.style.borderColor = ''; 25 element.style.boxShadow = ''; 26 }, 1500); 27 } 28 29 // Add new task 30 function addTask() { 31 const taskName = document.getElementById('taskName').value.trim(); 32 const taskDeadline = document.getElementById('taskDeadline').value; 33 34 // Check if required fields have value 35 if (taskName === '') { 36 showError('taskName'); 37 } 38 if (taskDeadline === '') { 39 showError('taskDeadline'); 40 } 41 42 // Stop if fields are empty 43 if (taskName === '' || taskDeadline === '') { 44 return; 45 } 46 47 // Get form values 48 let taskHours = document.getElementById('taskHours').value; 49 if (taskHours === '') { 50 taskHours = '?'; 51 } 52 53 let taskType = document.getElementById('taskType').value; 54 if (taskType === '') { 55 taskType = 'Other'; 56 } 57 58 const taskSubject = document.getElementById('taskSubject').value.trim(); 59 const taskDifficulty = getRadioValue('diff'); 60 const taskWorkType = getRadioValue('ptype'); 61 const taskWeight = getRadioValue('weight'); 62 const taskNotes = document.getElementById('taskNotes').value.trim(); 63 64 // Create task object 65 const newTask = { 66 id: Date.now(), 67 name: taskName, 68 deadline: taskDeadline, 69 hours: taskHours, 70 type: taskType, 71 subject: taskSubject, 72 difficulty: taskDifficulty, 73 workType: taskWorkType, 74 weight: taskWeight, 75 notes: taskNotes 76 }; 77 78 // Add task to list 79 tasks.push(newTask); 80 81 // Update display 82 renderList(); 83 resetForm(); 84 } 85 86 // Clear form to empty state 87 function resetForm() { 88 // Clear text inputs 89 document.getElementById('taskName').value = ''; 90 document.getElementById('taskHours').value = ''; 91 document.getElementById('taskSubject').value = ''; 92 document.getElementById('taskNotes').value = ''; 93 document.getElementById('taskDeadline').value = ''; 94 document.getElementById('taskType').value = ''; 95 96 // Reset radio buttons to defaults 97 document.querySelector('input[name="diff"][value="Medium"]').checked = true; 98 document.querySelector('input[name="ptype"][value="Individual"]').checked = true; 99 document.querySelector('input[name="weight"][value="Medium"]').checked = true; 100 } 101 102 // Remove task by ID 103 function removeTask(taskId) { 104 // Create new array without the deleted task 105 const newTasks = []; 106 for (let i = 0; i < tasks.length; i++) { 107 if (tasks[i].id !== taskId) { 108 newTasks.push(tasks[i]); 109 } 110 } 111 tasks = newTasks; 112 renderList(); 113 } 114 115 // Calculate days until deadline 116 function daysLeft(deadline) { 117 const today = new Date(); 118 const dueDate = new Date(deadline); 119 const difference = dueDate - today; 120 const millisecondsInDay = 86400000; 121 const days = Math.ceil(difference / millisecondsInDay); 122 return days; 123 } 124 125 // Show task list 126 function renderList() { 127 const taskCount = tasks.length; 128 129 // Update task count text 130 let countText = taskCount + ' task'; 131 if (taskCount !== 1) { 132 countText = taskCount + ' tasks'; 133 } 134 document.getElementById('taskCount').textContent = countText; 135 136 // Enable/disable analyze button 137 if (taskCount === 0) { 138 document.getElementById('analyzeBtn').disabled = true; 139 } else { 140 document.getElementById('analyzeBtn').disabled = false; 141 } 142 143 // Show empty state if no tasks 144 if (taskCount === 0) { 145 const emptyMessage = '<div class="empty-st"><div class="empty-icon">🗒️</div>No tasks yet.<br>Fill out the form above and click <strong style="color:var(--text)">"Add to List"</strong></div>'; 146 document.getElementById('taskList').innerHTML = emptyMessage; 147 return; 148 } 149 150 // Build HTML for each task 151 let listHTML = ''; 152 for (let i = 0; i < tasks.length; i++) { 153 const task = tasks[i]; 154 const taskNumber = i + 1; 155 const daysRemaining = daysLeft(task.deadline); 156 157 // Determine deadline style 158 let deadlineClass = 'deadline'; 159 let deadlineText = ''; 160 if (daysRemaining < 0) { 161 deadlineClass = 'overdue'; 162 deadlineText = 'Overdue ' + Math.abs(daysRemaining) + 'd'; 163 } else if (daysRemaining === 0) { 164 deadlineClass = 'deadline'; 165 deadlineText = 'Due today!'; 166 } else { 167 deadlineClass = 'deadline'; 168 deadlineText = daysRemaining + 'd left'; 169 } 170 171 // Determine difficulty style 172 let difficultyClass = 'medium'; 173 if (task.difficulty === 'Easy') { 174 difficultyClass = 'easy'; 175 } else if (task.difficulty === 'Hard') { 176 difficultyClass = 'hard'; 177 } 178 179 // Determine work type icon and style 180 let workTypeClass = 'solo'; 181 let workTypeIcon = '👤'; 182 if (task.workType === 'Group') { 183 workTypeClass = 'group'; 184 workTypeIcon = '👥'; 185 } 186 187 // Build subject chip if exists 188 let subjectChip = ''; 189 if (task.subject !== '') { 190 subjectChip = '<span class="chip chip-subject">' + task.subject + '</span>'; 191 } 192 193 // Build task card HTML 194 const taskCard = '<div class="task-card">' + 195 '<span class="task-idx">' + taskNumber + '.</span>' + 196 '<div class="task-body">' + 197 '<div class="task-name-text" title="' + task.name + '">' + task.name + '</div>' + 198 '<div class="task-chips">' + 199 '<span class="chip chip-' + deadlineClass + '"><svg width="10" height="10" viewBox="0 0 10 10" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" style="flex-shrink:0"><circle cx="5" cy="5" r="4"/><polyline points="5,2.5 5,5 6.5,6.5"/></svg> ' + deadlineText + '</span>' + 200 '<span class="chip chip-type">' + task.type + '</span>' + 201 '<span class="chip chip-' + workTypeClass + '">' + workTypeIcon + ' ' + task.workType + '</span>' + 202 '<span class="chip chip-' + difficultyClass + '">' + task.difficulty + '</span>' + 203 subjectChip + 204 '</div>' + 205 '</div>' + 206 '<button class="task-del" onclick="removeTask(' + task.id + ')"><svg width="12" height="12" viewBox="0 0 12 12" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"><line x1="2" y1="2" x2="10" y2="10"/><line x1="10" y1="2" x2="2" y2="10"/></svg></button>' + 207 '</div>'; 208 209 listHTML = listHTML + taskCard; 210 } 211 212 document.getElementById('taskList').innerHTML = listHTML; 213 } 214 215 // Analyze tasks with AI 216 async function analyzeTask() { 217 // Check if there are no tasks in the tasks array. 218 // If there are zero tasks, stop the function using return. 219 220 // Hide the placeholder element so the result area becomes empty. 221 // Target element id: "placeholder" 222 // Change its display style to "none". 223 224 // Show the loading animation. 225 // Target element id: "loadingWrap" 226 // Add the CSS class "show". 227 228 // Disable the analyze button so the user cannot click it again during loading. 229 // Target element id: "analyzeBtn" 230 231 try { 232 233 // Create an empty string variable that will store the formatted task information. 234 let taskData = ''; 235 236 // Loop through every task inside the tasks array. 237 // Use a for loop with index i. 238 for (let i = 0; i < tasks.length; i++) { 239 240 const task = tasks[i]; 241 242 // Create a task number starting from 1 instead of 0. 243 const index = 244 245 // Calculate the remaining days before the deadline. 246 // Use the helper function daysLeft() and pass task.deadline. 247 const daysRemaining = 248 249 // This variable will store the readable deadline text. 250 let deadlineStatus = ''; 251 252 // Create a condition: 253 // If daysRemaining is less than 0 → the task is overdue. 254 // Show: "OVERDUE by X days". 255 // Otherwise show: "X days from today". 256 257 // If the task subject is empty (""), replace it with "not specified". 258 // Otherwise keep the original subject. 259 const subject = 260 261 // If the task notes are empty (""), replace them with "none". 262 // Otherwise keep the original notes. 263 const notes = 264 265 // Build the taskData string by adding information for each task. 266 // Include the following information exactly like this format: 267 // 268 // Task 1: 269 // - Name: ... 270 // - Subject: ... 271 // - Type: ... 272 // - Deadline: ... (deadlineStatus) 273 // - Estimated time: ... hours 274 // - Difficulty: ... 275 // - Work type: ... 276 // - Grade weight: ... 277 // - Notes: ... 278 // 279 // Use string concatenation to append this information to taskData. 280 281 } 282 283 // Create the AI prompt. 284 // The prompt should tell the AI that it is a study planner. 285 // It must include: 286 // - today's date (use the variable "today") 287 // - the formatted taskData string 288 // - instructions asking the AI to rank tasks by priority 289 // 290 // Store the final prompt inside a variable called prompt. 291 const prompt = 292 293 // Send the request to the Gemini API using fetch(). 294 const response = await fetch('PASTE_API_URL_HERE', { 295 296 // Use the POST method. 297 method: 298 299 // Add headers with Content-Type set to application/json. 300 headers: 301 302 // Convert the request body object into JSON using JSON.stringify(). 303 // The body should contain: 304 // - contents with the prompt text 305 // - generationConfig with temperature and maxOutputTokens 306 body: 307 308 }); 309 310 // Check if the API request failed. 311 // If response.ok is false, throw a new Error with message "API error". 312 313 // Convert the response into JSON format. 314 const data = 315 316 // Create a variable to store the AI response text. 317 let rawResponse = ''; 318 319 // Extract the AI text from the following location: 320 // data.candidates[0].content.parts[0].text 321 // Store the text inside rawResponse. 322 323 // If rawResponse is empty, throw a new Error with message "Empty response". 324 325 // Clean the JSON text by removing markdown formatting such as ```json 326 let cleanedJSON = rawResponse 327 .replace(/```json/g, '') 328 .replace(/```/g, '') 329 .trim(); 330 331 // Try to extract the JSON object if the AI added extra text. 332 // Use a regular expression to match the content between { and }. 333 let jsonMatch = cleanedJSON.match(/\{[\s\S]*\}/); 334 335 if (jsonMatch) { 336 cleanedJSON = jsonMatch[0]; 337 } 338 339 // Convert the cleaned JSON string into a JavaScript object. 340 // Use JSON.parse(). 341 let result = 342 343 // Display the result by calling the renderResult() function 344 // and passing the parsed result as the argument. 345 346 } catch (error) { 347 348 // If an error happens: 349 // 1. Hide the loading animation (remove "show" class from loadingWrap) 350 // 2. Show the placeholder again (display = "flex") 351 352 const errorMessage = error.message === '' ? 'Unknown error' : error.message; 353 354 const errorHTML = 355 '<div class="r-glyph">🌧️</div>' + 356 '<div class="r-title">Something went wrong</div>' + 357 '<div class="r-sub">' + errorMessage + '</div>'; 358 359 // Insert the errorHTML into the placeholder element. 360 361 console.error(error); 362 } 363 364 // Re-enable the Analyze button by setting disabled to false. 365 366 } 367 368 // Convert urgency level to CSS class 369 function getUrgencyClass(urgencyLevel) { 370 if (urgencyLevel === 'Critical') { 371 return 'critical'; 372 } else if (urgencyLevel === 'High') { 373 return 'high'; 374 } else if (urgencyLevel === 'Medium') { 375 return 'medium'; 376 } else { 377 return 'low'; 378 } 379 } 380 381 // Get rank badge class 382 function getRankClass(index) { 383 if (index === 0) { 384 return 'rk-1'; 385 } else if (index === 1) { 386 return 'rk-2'; 387 } else if (index === 2) { 388 return 'rk-3'; 389 } else { 390 return 'rk-n'; 391 } 392 } 393 394 // Find task by name 395 function findTaskByName(taskName) { 396 for (let i = 0; i < tasks.length; i++) { 397 if (tasks[i].name === taskName) { 398 return tasks[i]; 399 } 400 } 401 return null; 402 } 403 404 // Display results 405 function renderResult(data) { 406 document.getElementById('loadingWrap').classList.remove('show'); 407 408 // Show timestamp 409 const now = new Date(); 410 const timeString = now.toLocaleTimeString('en-US', {hour: '2-digit', minute: '2-digit'}); 411 const taskWord = tasks.length === 1 ? 'task' : 'tasks'; 412 document.getElementById('resultMeta').textContent = tasks.length + ' ' + taskWord + ' · ' + timeString; 413 414 // Show strategy 415 const strategyHTML = '<div class="strategy-box"><div class="strategy-lbl">✦ Weekly Strategy</div><div class="strategy-text">' + data.summary + '</div></div>'; 416 document.getElementById('strategyWrap').innerHTML = strategyHTML; 417 418 // Build priority list 419 let prioritiesHTML = ''; 420 for (let i = 0; i < data.priorities.length; i++) { 421 const priority = data.priorities[i]; 422 const originalTask = findTaskByName(priority.taskName); 423 424 const rankClass = getRankClass(i); 425 const urgencyClass = getUrgencyClass(priority.urgencyLevel); 426 const score = priority.urgencyScore || 50; 427 428 // Get deadline info from original task 429 let deadlineChip = ''; 430 if (originalTask !== null) { 431 const daysRemaining = daysLeft(originalTask.deadline); 432 let deadlineClass = 'deadline'; 433 let deadlineText = ''; 434 435 if (daysRemaining < 0) { 436 deadlineClass = 'overdue'; 437 deadlineText = 'Overdue ' + Math.abs(daysRemaining) + 'd'; 438 } else if (daysRemaining === 0) { 439 deadlineClass = 'deadline'; 440 deadlineText = 'Due today!'; 441 } else { 442 deadlineClass = 'deadline'; 443 deadlineText = daysRemaining + 'd left'; 444 } 445 446 deadlineChip = '<span class="chip chip-' + deadlineClass + '"><svg width="10" height="10" viewBox="0 0 10 10" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" style="flex-shrink:0"><circle cx="5" cy="5" r="4"/><polyline points="5,2.5 5,5 6.5,6.5"/></svg> ' + deadlineText + '</span>'; 447 } 448 449 // Get type chip 450 let typeChip = ''; 451 if (originalTask !== null && originalTask.type !== '') { 452 typeChip = '<span class="chip chip-type">' + originalTask.type + '</span>'; 453 } 454 455 // Get subject chip 456 let subjectChip = ''; 457 if (originalTask !== null && originalTask.subject !== '') { 458 subjectChip = '<span class="chip chip-subject">' + originalTask.subject + '</span>'; 459 } 460 461 // Get tip section 462 let tipHTML = ''; 463 if (priority.tip !== '' && priority.tip !== undefined) { 464 tipHTML = '<div class="tip-row"><svg width="13" height="13" viewBox="0 0 13 13" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" style="flex-shrink:0;margin-top:2px"><path d="M6.5 1.5a4 4 0 0 1 2 7.4V10a .5.5 0 0 1-.5.5h-3A.5.5 0 0 1 4.5 10V8.9a4 4 0 0 1 2-7.4z"/><line x1="4.5" y1="11.5" x2="8.5" y2="11.5"/></svg><span>' + priority.tip + '</span></div>'; 465 } 466 467 // Build priority item HTML 468 const priorityDelay = i * 0.07; 469 const priorityHTML = '<div class="p-item" style="animation-delay:' + priorityDelay + 's">' + 470 '<div class="p-top">' + 471 '<div class="rank-badge ' + rankClass + '">' + priority.rank + '</div>' + 472 '<div style="flex:1;min-width:0">' + 473 '<div class="p-name">' + priority.taskName + '</div>' + 474 '<div class="p-chips">' + 475 deadlineChip + 476 typeChip + 477 subjectChip + 478 '</div>' + 479 '</div>' + 480 '</div>' + 481 '<div class="urg-row">' + 482 '<span class="urg-lbl">' + priority.urgencyLevel + '</span>' + 483 '<div class="urg-track"><div class="urg-fill ' + urgencyClass + '" style="width:0%" data-w="' + score + '%"></div></div>' + 484 '<span class="urg-score">' + score + '</span>' + 485 '</div>' + 486 '<div class="reason-box">' + 487 '<div class="reason-head">AI Reasoning</div>' + 488 '<div class="reason-text">' + priority.reason + '</div>' + 489 tipHTML + 490 '</div>' + 491 '</div>'; 492 493 prioritiesHTML = prioritiesHTML + priorityHTML; 494 } 495 496 document.getElementById('priorityList').innerHTML = prioritiesHTML; 497 document.getElementById('resultBody').classList.add('show'); 498 499 // Animate progress bars 500 setTimeout(function() { 501 const bars = document.querySelectorAll('.urg-fill[data-w]'); 502 for (let i = 0; i < bars.length; i++) { 503 const width = bars[i].getAttribute('data-w'); 504 bars[i].style.width = width; 505 } 506 }, 10); 507 }