-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathscript.js
More file actions
1596 lines (1351 loc) · 57.1 KB
/
Copy pathscript.js
File metadata and controls
1596 lines (1351 loc) · 57.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
if (sessionStorage.getItem('pp_uiStarted')) {
document.documentElement.classList.add('pp-skip-home');
}
document.addEventListener('DOMContentLoaded', () => {
const homepage = document.getElementById('homepage');
const startChatBtn = document.getElementById('start-chat-btn');
const topBar = document.querySelector('.top-bar');
const layoutBox = document.querySelector('.layout');
const adminBanner = document.getElementById('admin-banner');
const suError = document.getElementById('su-error');
topBar.classList.add('hidden');
layoutBox.classList.add('hidden');
adminBanner.classList.add('hidden');
const showChatUi = () => {
// Always open chat UI, no login check here!
homepage.classList.add('animate-out');
homepage.addEventListener('animationend', () => {
homepage.classList.add('hidden');
homepage.classList.remove('animate-out');
sessionStorage.setItem('pp_uiStarted', '1');
topBar.classList.remove('hidden');
topBar.classList.add('animate-in');
layoutBox.classList.remove('hidden');
layoutBox.classList.add('animate-in');
if (!adminBanner.classList.contains('hidden'))
adminBanner.classList.add('animate-in');
[topBar, layoutBox, adminBanner].forEach(el =>
el.addEventListener('animationend', () => el.classList.remove('animate-in'), { once: true })
);
}, { once: true });
};
if (startChatBtn) startChatBtn.addEventListener('click', showChatUi);
const messagesBox = document.getElementById('messages');
const diffBox = document.getElementById('difficulty-buttons');
const quoteBlock = document.querySelector('.quote');
const userInput = document.getElementById('user-input');
const submitCodeBtn = document.getElementById('submit-code-btn');
const hintBtn = document.getElementById('hint-btn');
const hintHelp = document.getElementById('hint-help');
const hintWrapper = document.querySelector('.hint-wrapper');
const topicsList = document.getElementById('topics-list');
const loginBtn = document.getElementById('login-btn');
const loginModal = document.getElementById('login-modal');
const modalClose = document.getElementById('modal-close');
const userTab = document.getElementById('user-tab');
const adminTab = document.getElementById('admin-tab');
const loginForm = document.getElementById('login-form');
const signupForm = document.getElementById('signup-form');
const goSignup = document.getElementById('go-signup');
const goLogin = document.getElementById('go-login');
const loginError = document.getElementById('login-error');
const adminForm = document.getElementById('admin-form');
const adminAttemptsInfo = document.getElementById('admin-attempts');
const profileDiv = document.getElementById('profile');
const userNameSp = document.getElementById('user-name');
const logoutBtn = document.getElementById('logout-btn');
const uploadBtn = document.getElementById('upload-syllabus-btn');
const fileInput = document.getElementById('syllabus-file');
const scoreBtn = document.getElementById('score-btn');
const scoreCntSp = document.getElementById('score-count');
const scoreModal = document.getElementById('score-modal');
const scoreClose = document.getElementById('score-close');
const scoreText = document.getElementById('score-text');
let solvedCount = 0;
const chats = {}; // { topicKey: [outerHTML,…] }
const lastTasks = {}; // { topicKey: rawTaskJSON }
const lastDifficulty = {}; // { topicKey: 'beginner' | 'medium' | 'hard' }
let currentTopicKey = null; // snake_case ключ выбранной темы
let attemptMade = false;
let isPageRefreshing = false; // Flag to prevent saving error messages during refresh
// Helper functions for chat persistence
const saveChatsToStorage = () => {
const userName = localStorage.getItem('pp_userName');
if (userName) {
localStorage.setItem(`pp_chats_${userName}`, JSON.stringify(chats));
}
};
const loadChatsFromStorage = () => {
const userName = localStorage.getItem('pp_userName');
if (userName) {
const savedChats = localStorage.getItem(`pp_chats_${userName}`);
console.log('Loading chats from localStorage for user:', userName);
console.log('Saved chats data:', savedChats);
if (savedChats) {
const parsedChats = JSON.parse(savedChats);
console.log('Parsed chats:', parsedChats);
// Clean up stale generating messages from saved chats
Object.keys(parsedChats).forEach(topicKey => {
if (parsedChats[topicKey]) {
parsedChats[topicKey] = parsedChats[topicKey].filter(message => {
return !message.includes('⏳ Generating your exercise, please wait…');
});
}
});
Object.keys(parsedChats).forEach(key => {
chats[key] = parsedChats[key];
});
console.log('Loaded chats into memory:', Object.keys(chats));
} else {
console.log('No saved chats found for user:', userName);
}
}
};
const clearChatsFromStorage = () => {
const userName = localStorage.getItem('pp_userName');
if (userName) {
localStorage.removeItem(`pp_chats_${userName}`);
}
};
// Helper functions for topic selection persistence
const saveSelectedTopicToStorage = () => {
const userName = localStorage.getItem('pp_userName');
if (userName && selectedTopic) {
localStorage.setItem(`pp_selectedTopic_${userName}`, selectedTopic);
}
};
const loadSelectedTopicFromStorage = () => {
const userName = localStorage.getItem('pp_userName');
if (userName) {
const savedTopic = localStorage.getItem(`pp_selectedTopic_${userName}`);
if (savedTopic) {
selectedTopic = savedTopic;
currentTopicKey = savedTopic.toLowerCase().replace(/\s+/g, '_');
return savedTopic;
}
}
return null;
};
const clearSelectedTopicFromStorage = () => {
const userName = localStorage.getItem('pp_userName');
if (userName) {
localStorage.removeItem(`pp_selectedTopic_${userName}`);
}
};
const scoreKey = () => 'pp_solved_' + (userNameSp.textContent || 'anon');
const loadScore = async () => {
try {
const token = localStorage.getItem('pp_token');
if (!token) {
console.log('No token found, setting score to 0');
solvedCount = 0;
updateScoreDisplay();
return;
}
console.log('Fetching score from backend...');
const res = await fetch(apiUrl('/get_stats'), {
headers: getAuthHeaders()
});
console.log('Score response status:', res.status);
if (res.ok) {
const stats = await res.json();
console.log('Backend stats received:', stats);
const oldScore = solvedCount;
solvedCount = stats.total || 0;
console.log(`Score updated: ${oldScore} → ${solvedCount}`);
updateScoreDisplay();
} else {
console.error('Failed to load score from backend, status:', res.status);
solvedCount = 0;
updateScoreDisplay();
}
} catch (err) {
console.error('Error loading score:', err);
solvedCount = 0;
updateScoreDisplay();
}
};
// Remove saveScore function since backend handles score updates
// const saveScore = () => localStorage.setItem(scoreKey(), solvedCount);
const updateScoreDisplay = () => {
console.log('Updating score display, current score:', solvedCount);
scoreCntSp.textContent = solvedCount;
scoreText.textContent =
`You have solved ${solvedCount} task${solvedCount === 1 ? '' : 's'} 🎉`;
console.log('Score display updated');
};
scoreBtn.addEventListener('click', () => {
updateScoreDisplay();
scoreModal.classList.remove('hidden');
});
scoreClose.addEventListener('click',
() => scoreModal.classList.add('hidden'));
let clearBtn = null;
let selectedTopic = null;
let currentDifficulty = null;
let currentTaskRaw = '';
let isAdmin = false;
let syllabusLoaded = false;
let diffPromptMsg = null;
const submittingTopics = new Set(); // Track loading state per topic
const disabledTopics = new Set(); // Track which topics have disabled inputs
const generatingTasks = new Set(); // Track task generation state per topic
const topicHints = {}; // { topicKey: { hints: [...], count: number } }
let currentlyGeneratingTopic = null; // Track which topic is currently generating
profileDiv.style.display = 'none';
logoutBtn.style.display = 'none';
userInput.disabled = true;
submitCodeBtn.disabled = true;
hintBtn.disabled = true;
topicsList.innerHTML = '';
topicsList.style.display = 'none';
// Restore login state from localStorage
const restoreLoginState = async () => {
const isLoggedIn = localStorage.getItem('pp_loggedIn') === 'true';
if (isLoggedIn) {
const userName = localStorage.getItem('pp_userName') || 'User';
const isAdmin = localStorage.getItem('pp_isAdmin') === 'true';
await finishLogin(userName, isAdmin);
}
};
const noTopicsMsg = document.createElement('div');
noTopicsMsg.textContent = '⏳ Please wait until the administrator uploads the syllabus 😔';
noTopicsMsg.style.cssText = 'color:#999;text-align:center;margin-top:16px;font-size:14px;';
topicsList.parentNode.insertBefore(noTopicsMsg, topicsList.nextSibling);
const hideQuote = () => quoteBlock && (quoteBlock.style.display = 'none');
const showMessage = (text, role = 'bot') => {
const div = document.createElement('div');
div.className = `message ${role}`;
div.textContent = text;
messagesBox.appendChild(div);
messagesBox.scrollTop = messagesBox.scrollHeight;
if (currentTopicKey) { // тема уже выбрана
if (!chats[currentTopicKey]) chats[currentTopicKey] = [];
chats[currentTopicKey].push(div.outerHTML);
console.log('Saving message to chat key:', currentTopicKey, 'Total messages for this topic:', chats[currentTopicKey].length);
saveChatsToStorage(); // Save to localStorage
}
return div;
};
const pushToChat = (text, role, topicKey) => {
if (!chats[topicKey]) chats[topicKey] = [];
const div = document.createElement('div');
div.className = `message ${role}`;
div.textContent = text;
chats[topicKey].push(div.outerHTML);
saveChatsToStorage(); // Save to localStorage
if (topicKey === currentTopicKey) { // чат открыт
messagesBox.appendChild(div);
messagesBox.scrollTop = messagesBox.scrollHeight;
}
};
const pushUserCode = (code, topicKey) => {
const div = document.createElement('div');
div.className = 'message user';
const pre = document.createElement('pre');
pre.textContent = code;
div.appendChild(pre);
if (!chats[topicKey]) chats[topicKey] = [];
chats[topicKey].push(div.outerHTML);
saveChatsToStorage(); // Save to localStorage
if (topicKey === currentTopicKey) {
messagesBox.appendChild(div);
messagesBox.scrollTop = messagesBox.scrollHeight;
}
};
const makeWaitingNotice = txt => {
const div = document.createElement('div');
div.className = 'message bot';
div.textContent = txt;
messagesBox.appendChild(div);
messagesBox.scrollTop = messagesBox.scrollHeight;
return () => div.remove();
};
const showCodeMessage = code => {
const d = document.createElement('div');
d.className = 'message user';
const p = document.createElement('pre');
p.textContent = code;
d.appendChild(p);
messagesBox.appendChild(d);
messagesBox.scrollTop = messagesBox.scrollHeight;
};
const fetchEval = async (url, opts={}) => {
const r = await fetch(url, opts);
if (!r.ok) throw new Error(await r.text());
return r.json();
};
// Helper function to create authenticated fetch options
const getAuthHeaders = (includeAuth = true) => {
const headers = { 'Content-Type': 'application/json' };
if (includeAuth) {
const token = localStorage.getItem('pp_token');
if (token) {
headers['Authorization'] = `Bearer ${token}`;
}
}
return headers;
};
// Helper function to validate session and refresh if needed
const validateSession = async () => {
const token = localStorage.getItem('pp_token');
if (!token) {
return false;
}
try {
const res = await fetch(apiUrl('/get_stats'), {
headers: getAuthHeaders()
});
if (res.status === 401) {
// Token expired or invalid
localStorage.removeItem('pp_token');
localStorage.removeItem('pp_loggedIn');
localStorage.removeItem('pp_userName');
localStorage.removeItem('pp_isAdmin');
return false;
}
return res.ok;
} catch (err) {
console.error('Session validation error:', err);
return false;
}
};
const updateTopicList = arr => {
syllabusLoaded = arr.length > 0;
topicsList.innerHTML = '';
// Get current admin status from localStorage to ensure it's correct
const currentIsAdmin = localStorage.getItem('pp_isAdmin') === 'true';
if (!syllabusLoaded) {
topicsList.style.display = 'none';
noTopicsMsg.style.display = currentIsAdmin ? 'none' : 'block';
userInput.disabled = true;
submitCodeBtn.disabled = true;
selectedTopic = null;
if (currentIsAdmin) {
uploadBtn.style.display = 'block';
if (clearBtn) clearBtn.style.display = 'none';
}
return;
}
topicsList.style.display = 'block';
noTopicsMsg.style.display = 'none';
userInput.disabled = false;
submitCodeBtn.disabled = false;
diffBox.style.display = 'flex';
arr.forEach(t => {
const li = document.createElement('li');
li.textContent = t.trim();
topicsList.appendChild(li);
li.addEventListener('click', () => handleTopic(li));
});
if (currentIsAdmin) {
if (!clearBtn) {
clearBtn = document.createElement('button');
clearBtn.id = 'clear-syllabus-btn';
clearBtn.className = 'upload-btn';
clearBtn.textContent = 'Clear syllabus';
clearBtn.style.marginTop = '6px';
clearBtn.addEventListener('click', clearSyllabus);
uploadBtn.parentNode.insertBefore(clearBtn, uploadBtn.nextSibling);
}
clearBtn.style.display = 'block';
uploadBtn.style.display = 'none';
}
};
function clearChat() {
messagesBox.innerHTML = '';
taskShown = false;
answerSent = false;
hintBtn.disabled = true;
if (quoteBlock) quoteBlock.style.display = 'none';
}
const updateInputStates = () => {
console.log('updateInputStates called, currentTopicKey:', currentTopicKey);
if (!currentTopicKey) {
console.log('No currentTopicKey, keeping inputs in default state');
return;
}
const hasTask = Boolean(lastTasks[currentTopicKey]);
// Enable submit button only if there's a task, otherwise allow difficulty selection
submitCodeBtn.disabled = !hasTask;
userInput.disabled = false; // Always allow typing
hintBtn.disabled = !hasTask; // Enable hint button only if there's a task
console.log(`Topic: ${currentTopicKey}, hasTask: ${hasTask}, submitBtn disabled: ${!hasTask}, hintBtn disabled: ${!hasTask}`);
};
const handleTopic = li => {
// Check if user is logged in before allowing topic selection
const isLoggedIn = localStorage.getItem('pp_loggedIn') === 'true';
if (!isLoggedIn) {
showMessage('❗️ Please log in to use the bot', 'bot');
openModal();
return;
}
if (!syllabusLoaded) return;
hideQuote();
li.classList.remove('has-new');
/* 1. подчёркиваем активную тему */
document.querySelectorAll('.sidebar li')
.forEach(e => e.classList.remove('active-topic'));
li.classList.add('active-topic');
/* 2. сохраняем DOM-историю прежней темы */
if (currentTopicKey) {
console.log('Saving current topic chat before switching. Current topic:', currentTopicKey, 'Messages:', messagesBox.children.length);
chats[currentTopicKey] = Array.from(
messagesBox.children,
el => el.outerHTML
);
console.log('Saved chat for topic:', currentTopicKey, 'Messages saved:', chats[currentTopicKey].length);
}
/* 3. вычисляем новый ключ */
selectedTopic = li.textContent.trim();
currentTopicKey = selectedTopic.toLowerCase().replace(/\s+/g, '_');
console.log('Generated topic key:', currentTopicKey, 'from topic:', selectedTopic);
// Save selected topic to localStorage
saveSelectedTopicToStorage();
/* 4. вытаскиваем кэш-данные */
currentDifficulty = lastDifficulty[currentTopicKey] ?? null;
currentTaskRaw = lastTasks[currentTopicKey] ?? '';
hintBtn.disabled = !currentTaskRaw;
submitCodeBtn.disabled = !currentTaskRaw;
diffBox.style.display = 'flex';
/* 5. восстанавливаем или очищаем чат */
messagesBox.innerHTML = '';
console.log('Restoring chat for topic:', currentTopicKey);
console.log('Available chats:', Object.keys(chats));
console.log('Chat for current topic:', chats[currentTopicKey]);
console.log('Chat object before restoration:', JSON.stringify(chats, null, 2));
if (chats[currentTopicKey] && chats[currentTopicKey].length > 0) {
console.log('Restoring', chats[currentTopicKey].length, 'messages for topic:', currentTopicKey);
messagesBox.innerHTML = chats[currentTopicKey].join('');
messagesBox.scrollTop = messagesBox.scrollHeight;
} else {
console.log('No saved chat for topic:', currentTopicKey, '- showing initial messages');
showMessage(selectedTopic, 'user');
if (!currentTaskRaw)
diffPromptMsg = showMessage('Select difficulty 👇', 'bot');
}
// Check if this topic was generating and re-enable buttons if needed
if (currentlyGeneratingTopic === currentTopicKey) {
const difficultyButtons = document.querySelectorAll('#difficulty-buttons button');
difficultyButtons.forEach(btn => btn.disabled = true);
console.log('Buttons remain disabled for topic that is generating');
} else {
const difficultyButtons = document.querySelectorAll('#difficulty-buttons button');
difficultyButtons.forEach(btn => btn.disabled = false);
console.log('Buttons enabled for topic that is not generating');
}
const hasTask = Boolean(lastTasks[currentTopicKey]);
submitCodeBtn.disabled = !hasTask; // разрешаем «Send code», если задача есть
hintBtn.disabled = !hasTask; // то же для «Hint»
diffBox.style.display = 'flex'; // изменила здесь
updateInputStates();
};
// Function to fetch syllabus with authentication
const fetchSyllabus = async () => {
const token = localStorage.getItem('pp_token');
if (!token) {
console.log('No token found, cannot fetch syllabus');
return;
}
console.log('Fetching syllabus from backend...');
try {
const res = await fetch(apiUrl('/get_syllabus'), {
headers: getAuthHeaders()
});
console.log('Syllabus response status:', res.status);
if (res.ok) {
const data = await res.json();
console.log('Syllabus data received:', data);
if (data && Array.isArray(data.topics)) {
console.log('Updating topic list with:', data.topics);
updateTopicList(data.topics);
// Restore selected topic after syllabus is loaded
const savedTopic = loadSelectedTopicFromStorage();
console.log('Attempting to restore saved topic:', savedTopic);
console.log('Available topics:', data.topics);
console.log('Current chats in memory:', Object.keys(chats));
if (savedTopic && data.topics.includes(savedTopic)) {
console.log('Found saved topic in syllabus, restoring...');
// Find the topic list item and restore directly without calling handleTopic
const topicItems = document.querySelectorAll('#topics-list li');
for (let li of topicItems) {
if (li.textContent.trim() === savedTopic) {
console.log('Found topic list item, restoring directly...');
// Set the topic variables directly
selectedTopic = li.textContent.trim();
currentTopicKey = selectedTopic.toLowerCase().replace(/\s+/g, '_');
console.log('Generated topic key:', currentTopicKey, 'from topic:', selectedTopic);
// Highlight the topic in UI
document.querySelectorAll('.sidebar li').forEach(e => e.classList.remove('active-topic'));
li.classList.add('active-topic');
// Restore chat messages directly
messagesBox.innerHTML = '';
console.log('Restoring chat for topic:', currentTopicKey);
console.log('Available chats:', Object.keys(chats));
console.log('Chat for current topic:', chats[currentTopicKey]);
if (chats[currentTopicKey] && chats[currentTopicKey].length > 0) {
console.log('Restoring', chats[currentTopicKey].length, 'messages for topic:', currentTopicKey);
messagesBox.innerHTML = chats[currentTopicKey].join('');
messagesBox.scrollTop = messagesBox.scrollHeight;
} else {
console.log('No saved chat for topic:', currentTopicKey, '- showing initial messages');
showMessage(selectedTopic, 'user');
diffPromptMsg = showMessage('Select difficulty 👇', 'bot');
}
// Restore other topic data
currentDifficulty = lastDifficulty[currentTopicKey] ?? null;
currentTaskRaw = lastTasks[currentTopicKey] ?? '';
hintBtn.disabled = !currentTaskRaw;
submitCodeBtn.disabled = !currentTaskRaw;
diffBox.style.display = 'flex';
// Only enable difficulty buttons if no task exists yet
const difficultyButtons = document.querySelectorAll('#difficulty-buttons button');
if (!currentTaskRaw) {
difficultyButtons.forEach(btn => btn.disabled = false);
}
userInput.disabled = false;
// Update input states
updateInputStates();
break;
}
}
} else {
console.log('No saved topic found or topic not in syllabus');
}
} else {
console.log('No topics found in syllabus data');
updateTopicList([]);
}
} else {
console.error('Failed to fetch syllabus, status:', res.status);
const errorData = await res.json().catch(() => ({}));
console.error('Syllabus error details:', errorData);
updateTopicList([]);
}
} catch (err) {
console.error('Error fetching syllabus:', err);
updateTopicList([]);
}
};
const clearSyllabus = () => {
updateTopicList([]);
diffBox.style.display = 'flex';
fileInput.value = '';
// Clear all session data related to topics
Object.keys(chats).forEach(key => delete chats[key]);
Object.keys(lastTasks).forEach(key => delete lastTasks[key]);
Object.keys(lastDifficulty).forEach(key => delete lastDifficulty[key]);
Object.keys(topicHints).forEach(key => delete topicHints[key]);
submittingTopics.clear();
disabledTopics.clear();
generatingTasks.clear();
// Clear chat messages from localStorage
clearChatsFromStorage();
// Clear selected topic from localStorage
clearSelectedTopicFromStorage();
// Reset current session variables
selectedTopic = null;
currentTopicKey = null;
currentDifficulty = null;
currentTaskRaw = '';
syllabusLoaded = false;
diffPromptMsg = null;
currentlyGeneratingTopic = null;
attemptMade = false;
// Clear UI elements
messagesBox.innerHTML = '';
userInput.value = '';
userInput.disabled = true;
submitCodeBtn.disabled = true;
hintBtn.disabled = true;
diffBox.style.display = 'none';
// Clear any active topic selection
document.querySelectorAll('.sidebar li').forEach(li => {
li.classList.remove('active-topic');
});
// Show quote again
if (quoteBlock) quoteBlock.style.display = 'block';
fetch(apiUrl('/delete_syllabus'), {
method: 'DELETE',
headers: getAuthHeaders()
}).catch(()=>{});
alert('Syllabus cleared');
};
const openModal = () => loginModal.classList.remove('hidden');
const closeModal = () => loginModal.classList.add('hidden');
loginBtn.addEventListener('click', openModal);
modalClose.addEventListener('click', closeModal);
userTab.addEventListener('click', () => {
userTab.classList.add('active');
loginForm.classList.remove('hidden');
signupForm.classList.add('hidden');
});
goSignup.addEventListener('click', () => {
loginForm.classList.add('hidden'); signupForm.classList.remove('hidden');
loginError.textContent = '';
});
goLogin.addEventListener('click', () => {
signupForm.classList.add('hidden'); loginForm.classList.remove('hidden');
loginError.textContent = '';
});
const validateSignup = (name, email, pwd) => {
const reName = /^[a-zA-Z][a-zA-Z0-9_]{2,15}$/;
const reEmail = /^[^\s@]+@[^\s@]+\.[^\s@]{2,}$/;
if (!reName.test(name)) return 'Nickname must be 3-16 latin letters/digits';
if (!reEmail.test(email)) return 'Invalid e-mail format';
if (pwd.length < 9) return 'Password must be ≥ 9 chars';
if (!/[A-Z]/.test(pwd) || !/[a-z]/.test(pwd) || !/\d/.test(pwd))
return 'Password needs upper, lower & digit';
return '';
};
const LOCAL_HOSTNAMES = ['localhost', '127.0.0.1', ''];
const isLocal = LOCAL_HOSTNAMES.includes(location.hostname);
const getUsers = () => JSON.parse(localStorage.getItem('pp_users') || '[]');
const saveUsers = users => localStorage.setItem('pp_users', JSON.stringify(users));
const errorBox = suError || loginError;
const showSuErr = msg => { if (errorBox) errorBox.textContent = msg; };
signupForm.addEventListener('submit', async e => {
e.preventDefault();
const name = document.getElementById('su-name').value.trim();
const email = document.getElementById('su-email').value.trim();
const pwd = document.getElementById('su-password').value.trim();
const err = validateSignup(name, email, pwd);
if (err) { showSuErr(err); return; }
showSuErr('');
if (isLocal) {
const users = getUsers();
if (users.some(u => u.email === email || u.name === name)) {
showSuErr('User with this e-mail or nickname already exists');
return;
}
users.push({ name, email, pwd });
saveUsers(users);
await finishLogin(name, false);
closeModal();
return;
}
try {
const res = await fetch(apiUrl('/signup'), {
method : 'POST',
headers: { 'Content-Type': 'application/json' },
body : JSON.stringify({ username: name, email, password: pwd })
});
if (!res.ok) {
const data = await res.json().catch(()=>({}));
showSuErr(data.detail || `Server error (${res.status})`);
return;
}
// Do NOT log in the user automatically!
// const data = await res.json();
// if (data.token) localStorage.setItem('pp_token', data.token);
// await finishLogin(data.name || name, false);
// closeModal();
// Instead, show a message to check email for verification
showSuErr('✅ Registration successful! Please check your email to verify your account before logging in.');
} catch (e2) {
showSuErr(`Network error: ${e2.message}`);
}
});
loginForm.addEventListener('submit', async e => {
e.preventDefault();
const username = document.getElementById('li-identifier').value.trim();
const pwd = document.getElementById('li-password').value.trim();
if (!username || !pwd) return;
try {
const res = await fetch(apiUrl('/login'), {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username: username, password: pwd })
});
if (!res.ok) {
const err = await res.json();
loginError.textContent = err.detail || 'Login failed';
return;
}
const data = await res.json();
// Handle role-based login
const isAdmin = data.role === 'admin';
// Save token BEFORE calling finishLogin
if (data.token) localStorage.setItem('pp_token', data.token);
await finishLogin(username, isAdmin);
closeModal();
} catch (err) {
loginError.textContent = `Error: ${err.message}`;
}
});
const adjustLayoutHeight = () => {
const bannerHeight = adminBanner.classList.contains('hidden') ? 0 : adminBanner.offsetHeight;
layoutBox.style.height = `calc(100vh - 64px - ${bannerHeight}px)`;
};
const finishLogin = async (name, admin) => {
console.log('finishLogin called with:', { name, admin });
isAdmin = admin;
profileDiv.style.display = 'flex';
logoutBtn.style.display = 'inline-block';
notificationSettingsBtn.classList.remove('hidden');
notificationSettingsBtn.style.display = 'inline-block';
userNameSp.textContent = name;
loginBtn.style.display = 'none';
adminBanner.classList.toggle('hidden', !admin);
// Save login state to localStorage
localStorage.setItem('pp_loggedIn', 'true');
localStorage.setItem('pp_userName', name);
localStorage.setItem('pp_isAdmin', admin.toString());
// Load chat messages from localStorage
loadChatsFromStorage();
// Load selected topic from localStorage
const savedTopic = loadSelectedTopicFromStorage();
console.log('finishLogin: Loaded saved topic:', savedTopic);
console.log('finishLogin: Chats loaded:', Object.keys(chats));
// Show upload button for admin users
if (admin && !syllabusLoaded) {
uploadBtn.style.display = 'block';
} else {
uploadBtn.style.display = 'none';
}
// Show clear button for admin users when syllabus is loaded
if (clearBtn) clearBtn.style.display = 'none';
if (admin && syllabusLoaded) {
if (!clearBtn) {
clearBtn = document.createElement('button');
clearBtn.id = 'clear-syllabus-btn';
clearBtn.className = 'upload-btn';
clearBtn.textContent = 'Clear syllabus';
clearBtn.style.marginTop = '6px';
clearBtn.addEventListener('click', clearSyllabus);
uploadBtn.parentNode.insertBefore(clearBtn, uploadBtn.nextSibling);
}
clearBtn.style.display = 'block';
} else if (clearBtn) {
clearBtn.style.display = 'none';
}
// Hide "no topics" message for admin users
if (admin && !syllabusLoaded) noTopicsMsg.style.display = 'none';
scoreBtn.classList.remove('hidden');
notificationSettingsBtn.classList.remove('hidden');
console.log('Loading score...');
await loadScore();
console.log('Score loaded, now loading syllabus...');
closeModal();
adjustLayoutHeight();
// Fetch syllabus after login - await it to ensure it completes
await fetchSyllabus();
console.log('Syllabus loaded, login complete');
};
logoutBtn.addEventListener('click', () => {
isAdmin = false;
profileDiv.style.display = 'none';
logoutBtn.style.display = 'none';
notificationSettingsBtn.classList.add('hidden');
notificationSettingsBtn.style.display = 'none';
loginBtn.style.display = 'inline-block';
adminBanner.classList.add('hidden');
uploadBtn.style.display = 'none';
if (clearBtn) clearBtn.style.display = 'none';
if (!syllabusLoaded) noTopicsMsg.style.display = 'block';
adjustLayoutHeight();
scoreBtn.classList.add('hidden');
notificationSettingsBtn.classList.add('hidden');
localStorage.removeItem('pp_token'); // Remove token on logout
// Clear login state from localStorage
localStorage.removeItem('pp_loggedIn');
localStorage.removeItem('pp_userName');
localStorage.removeItem('pp_isAdmin');
// Clear all session data by deleting properties
Object.keys(chats).forEach(key => delete chats[key]);
Object.keys(lastTasks).forEach(key => delete lastTasks[key]);
Object.keys(lastDifficulty).forEach(key => delete lastDifficulty[key]);
Object.keys(topicHints).forEach(key => delete topicHints[key]);
submittingTopics.clear();
disabledTopics.clear();
generatingTasks.clear();
// Clear chat messages from localStorage
clearChatsFromStorage();
// Clear selected topic from localStorage
clearSelectedTopicFromStorage();
// Reset current session variables
selectedTopic = null;
currentTopicKey = null;
currentDifficulty = null;
currentTaskRaw = '';
syllabusLoaded = false;
diffPromptMsg = null;
currentlyGeneratingTopic = null;
attemptMade = false;
solvedCount = 0;
// Clear UI elements
messagesBox.innerHTML = '';
topicsList.innerHTML = '';
topicsList.style.display = 'none';
userInput.value = '';
userInput.disabled = true;
submitCodeBtn.disabled = true;
hintBtn.disabled = true;
diffBox.style.display = 'none';
// Reset score display
scoreCntSp.textContent = '0';
scoreText.textContent = 'You have solved 0 tasks 🎉';
// Clear any active topic selection
document.querySelectorAll('.sidebar li').forEach(li => {
li.classList.remove('active-topic');
});
// Show quote again
if (quoteBlock) quoteBlock.style.display = 'block';
// Hide main layout and show homepage
topBar.classList.add('hidden');
layoutBox.classList.add('hidden');
adminBanner.classList.add('hidden');
homepage.classList.remove('hidden');
// Show "no topics" message
if (noTopicsMsg) noTopicsMsg.style.display = 'block';
});
uploadBtn.addEventListener('click', () => fileInput.click());
fileInput.setAttribute('accept', '.txt,application/pdf');
fileInput.addEventListener('change', async e => {
const f = e.target.files[0];
if (!f) return;
const name = f.name.toLowerCase();
if (!name.endsWith('.txt') && !name.endsWith('.pdf')) {
return alert('Only .txt and .pdf files allowed');
}
// Create FormData and send the file directly
const formData = new FormData();
formData.append('file', f);
try {
const response = await fetch(apiUrl('/save_syllabus'), {
method: 'POST',
headers: {
'Authorization': `Bearer ${localStorage.getItem('pp_token')}`
// Don't set Content-Type - let browser set it with boundary for FormData
},
body: formData
});
console.log('Save syllabus response status:', response.status);
if (response.ok) {
const data = await response.json();
console.log('Syllabus saved successfully:', data);
// Update the UI with the topics returned from the backend
if (data && Array.isArray(data.topics)) {
updateTopicList(data.topics);
alert('Syllabus uploaded ✅');
} else {
alert('Syllabus uploaded but no topics returned');
}
} else {
const errorData = await response.json().catch(() => ({}));
console.error('Save syllabus failed:', errorData);
alert(`Failed to save syllabus: ${errorData.detail || response.statusText}`);
}
} catch (error) {
console.error('Network error saving syllabus:', error);
alert('Network error saving syllabus');
}
});
userInput.addEventListener('input', () => {
userInput.style.height = 'auto';
userInput.style.height = userInput.scrollHeight + 'px';
});