Spaces:
Sleeping
Sleeping
Update web/src/App.tsx
Browse files- web/src/App.tsx +60 -41
web/src/App.tsx
CHANGED
|
@@ -118,7 +118,9 @@ function App() {
|
|
| 118 |
|
| 119 |
const [user, setUser] = useState<User | null>(null);
|
| 120 |
|
| 121 |
-
const [currentCourseId, setCurrentCourseId] = useState<string>(
|
|
|
|
|
|
|
| 122 |
|
| 123 |
const availableCourses: CourseInfo[] = [
|
| 124 |
{
|
|
@@ -161,7 +163,8 @@ function App() {
|
|
| 161 |
{
|
| 162 |
id: "review-1",
|
| 163 |
role: "assistant",
|
| 164 |
-
content:
|
|
|
|
| 165 |
timestamp: new Date(),
|
| 166 |
},
|
| 167 |
]);
|
|
@@ -170,7 +173,8 @@ function App() {
|
|
| 170 |
{
|
| 171 |
id: "quiz-1",
|
| 172 |
role: "assistant",
|
| 173 |
-
content:
|
|
|
|
| 174 |
timestamp: new Date(),
|
| 175 |
},
|
| 176 |
]);
|
|
@@ -200,7 +204,9 @@ function App() {
|
|
| 200 |
|
| 201 |
const hasUserMessages = currentMessages.some((msg) => msg.role === "user");
|
| 202 |
const expectedWelcomeId = chatMode === "ask" ? "1" : chatMode === "review" ? "review-1" : "quiz-1";
|
| 203 |
-
const hasWelcomeMessage = currentMessages.some(
|
|
|
|
|
|
|
| 204 |
const modeChanged = prevChatModeRef.current !== chatMode;
|
| 205 |
|
| 206 |
if ((modeChanged || currentMessages.length === 0 || !hasWelcomeMessage) && !hasUserMessages) {
|
|
@@ -218,7 +224,8 @@ function App() {
|
|
| 218 |
{
|
| 219 |
id: "review-1",
|
| 220 |
role: "assistant",
|
| 221 |
-
content:
|
|
|
|
| 222 |
timestamp: new Date(),
|
| 223 |
},
|
| 224 |
],
|
|
@@ -226,7 +233,8 @@ function App() {
|
|
| 226 |
{
|
| 227 |
id: "quiz-1",
|
| 228 |
role: "assistant",
|
| 229 |
-
content:
|
|
|
|
| 230 |
timestamp: new Date(),
|
| 231 |
},
|
| 232 |
],
|
|
@@ -309,7 +317,6 @@ function App() {
|
|
| 309 |
}
|
| 310 |
}, [user, groupMembers, availableCourses]);
|
| 311 |
|
| 312 |
-
// β
stable fallback to avoid undefined props during the first render after login
|
| 313 |
const fallbackWorkspace: Workspace = {
|
| 314 |
id: "individual",
|
| 315 |
name: "My Space",
|
|
@@ -348,7 +355,7 @@ function App() {
|
|
| 348 |
localStorage.setItem("theme", isDarkMode ? "dark" : "light");
|
| 349 |
}, [isDarkMode]);
|
| 350 |
|
| 351 |
-
// β
lock
|
| 352 |
useEffect(() => {
|
| 353 |
const prev = document.body.style.overflow;
|
| 354 |
document.body.style.overflow = "hidden";
|
|
@@ -385,11 +392,13 @@ function App() {
|
|
| 385 |
question: "Which of the following is NOT a principle of Responsible AI?",
|
| 386 |
options: ["A) Fairness", "B) Transparency", "C) Profit Maximization", "D) Accountability"],
|
| 387 |
correctAnswer: "C",
|
| 388 |
-
explanation:
|
|
|
|
| 389 |
},
|
| 390 |
{
|
| 391 |
type: "fill-in-blank",
|
| 392 |
-
question:
|
|
|
|
| 393 |
correctAnswer: "protected characteristics",
|
| 394 |
explanation:
|
| 395 |
"Protected characteristics include attributes like race, gender, age, religion, etc. AI systems should not discriminate based on these.",
|
|
@@ -575,7 +584,6 @@ function App() {
|
|
| 575 |
setUploadedFiles((prev) => prev.filter((_, i) => i !== index));
|
| 576 |
};
|
| 577 |
|
| 578 |
-
// β
FIX: use the same "next" array for state update and for uploading; do NOT read stale `uploadedFiles`
|
| 579 |
const handleFileTypeChange = async (index: number, type: FileType) => {
|
| 580 |
if (!user) return;
|
| 581 |
|
|
@@ -612,7 +620,11 @@ function App() {
|
|
| 612 |
|
| 613 |
return chat.messages.every((savedMsg, idx) => {
|
| 614 |
const currentMsg = messages[idx];
|
| 615 |
-
return
|
|
|
|
|
|
|
|
|
|
|
|
|
| 616 |
});
|
| 617 |
}) || null
|
| 618 |
);
|
|
@@ -685,7 +697,8 @@ function App() {
|
|
| 685 |
{
|
| 686 |
id: "review-1",
|
| 687 |
role: "assistant",
|
| 688 |
-
content:
|
|
|
|
| 689 |
timestamp: new Date(),
|
| 690 |
},
|
| 691 |
],
|
|
@@ -693,7 +706,8 @@ function App() {
|
|
| 693 |
{
|
| 694 |
id: "quiz-1",
|
| 695 |
role: "assistant",
|
| 696 |
-
content:
|
|
|
|
| 697 |
timestamp: new Date(),
|
| 698 |
},
|
| 699 |
],
|
|
@@ -774,7 +788,12 @@ function App() {
|
|
| 774 |
toast.success("Removed from saved items");
|
| 775 |
};
|
| 776 |
|
| 777 |
-
const handleCreateWorkspace = (payload: {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 778 |
const id = `group-${Date.now()}`;
|
| 779 |
const avatar = `https://api.dicebear.com/7.x/shapes/svg?seed=${encodeURIComponent(payload.name)}`;
|
| 780 |
|
|
@@ -856,16 +875,17 @@ function App() {
|
|
| 856 |
const handleOnboardingSkip = () => setShowOnboarding(false);
|
| 857 |
|
| 858 |
if (!user) return <LoginScreen onLogin={handleLogin} />;
|
| 859 |
-
if (showOnboarding && user)
|
|
|
|
| 860 |
|
| 861 |
return (
|
| 862 |
-
// β
|
| 863 |
-
<div className="
|
| 864 |
<Toaster />
|
| 865 |
|
| 866 |
-
{/*
|
| 867 |
-
<div className="flex
|
| 868 |
-
{/* Header
|
| 869 |
<div className="flex-shrink-0">
|
| 870 |
<Header
|
| 871 |
user={user}
|
|
@@ -889,15 +909,16 @@ function App() {
|
|
| 889 |
<ProfileEditor user={user} onSave={setUser} onClose={() => setShowProfileEditor(false)} />
|
| 890 |
)}
|
| 891 |
|
| 892 |
-
{/* Review banner
|
| 893 |
{showReviewBanner && (
|
| 894 |
<div className="flex-shrink-0 w-full bg-background border-b border-border relative z-50">
|
| 895 |
<ReviewBanner onReview={handleReviewClick} onDismiss={handleDismissReviewBanner} />
|
| 896 |
</div>
|
| 897 |
)}
|
| 898 |
|
| 899 |
-
{/* Main
|
| 900 |
-
<div className="flex-1 min-h-0 min-w-0
|
|
|
|
| 901 |
{!leftPanelVisible && (
|
| 902 |
<Button
|
| 903 |
variant="secondary"
|
|
@@ -911,13 +932,14 @@ function App() {
|
|
| 911 |
</Button>
|
| 912 |
)}
|
| 913 |
|
|
|
|
| 914 |
{leftSidebarOpen && (
|
| 915 |
<div className="fixed inset-0 bg-black/50 z-40 lg:hidden" onClick={() => setLeftSidebarOpen(false)} />
|
| 916 |
)}
|
| 917 |
|
| 918 |
{/* Desktop left panel */}
|
| 919 |
{leftPanelVisible ? (
|
| 920 |
-
<aside className="hidden lg:flex w-80 h-full min-h-0 min-w-0 bg-card border-r border-border overflow-hidden relative">
|
| 921 |
<Button
|
| 922 |
variant="secondary"
|
| 923 |
size="icon"
|
|
@@ -929,7 +951,7 @@ function App() {
|
|
| 929 |
<ChevronLeft className="h-3 w-3" />
|
| 930 |
</Button>
|
| 931 |
|
| 932 |
-
{/*
|
| 933 |
<div className="flex-1 min-h-0 min-w-0 overflow-hidden">
|
| 934 |
<LeftSidebar
|
| 935 |
learningMode={learningMode}
|
|
@@ -960,19 +982,16 @@ function App() {
|
|
| 960 |
</aside>
|
| 961 |
) : null}
|
| 962 |
|
| 963 |
-
{/* Mobile left drawer */}
|
| 964 |
<aside
|
| 965 |
-
className={
|
| 966 |
-
fixed lg:hidden
|
| 967 |
-
|
| 968 |
-
|
| 969 |
-
|
| 970 |
-
|
| 971 |
-
|
| 972 |
-
|
| 973 |
-
overflow-hidden
|
| 974 |
-
flex flex-col
|
| 975 |
-
`}
|
| 976 |
>
|
| 977 |
<div className="p-4 border-b border-border flex justify-between items-center flex-shrink-0">
|
| 978 |
<h3>Settings & Guide</h3>
|
|
@@ -1010,9 +1029,9 @@ function App() {
|
|
| 1010 |
</div>
|
| 1011 |
</aside>
|
| 1012 |
|
| 1013 |
-
{/* Chat column:
|
| 1014 |
-
<main className="flex-1 min-w-0 min-h-0 overflow-hidden flex
|
| 1015 |
-
<div className="flex-1 min-h-0 overflow-hidden">
|
| 1016 |
<ChatArea
|
| 1017 |
messages={messages}
|
| 1018 |
onSendMessage={handleSendMessage}
|
|
|
|
| 118 |
|
| 119 |
const [user, setUser] = useState<User | null>(null);
|
| 120 |
|
| 121 |
+
const [currentCourseId, setCurrentCourseId] = useState<string>(
|
| 122 |
+
() => localStorage.getItem("myspace_selected_course") || "course1"
|
| 123 |
+
);
|
| 124 |
|
| 125 |
const availableCourses: CourseInfo[] = [
|
| 126 |
{
|
|
|
|
| 163 |
{
|
| 164 |
id: "review-1",
|
| 165 |
role: "assistant",
|
| 166 |
+
content:
|
| 167 |
+
"π Welcome to Review mode! I'll help you review and consolidate your learning. Let's go through what you've learned!",
|
| 168 |
timestamp: new Date(),
|
| 169 |
},
|
| 170 |
]);
|
|
|
|
| 173 |
{
|
| 174 |
id: "quiz-1",
|
| 175 |
role: "assistant",
|
| 176 |
+
content:
|
| 177 |
+
"π― Welcome to Quiz mode! I'll test your understanding with personalized questions based on your learning history. Ready to start?",
|
| 178 |
timestamp: new Date(),
|
| 179 |
},
|
| 180 |
]);
|
|
|
|
| 204 |
|
| 205 |
const hasUserMessages = currentMessages.some((msg) => msg.role === "user");
|
| 206 |
const expectedWelcomeId = chatMode === "ask" ? "1" : chatMode === "review" ? "review-1" : "quiz-1";
|
| 207 |
+
const hasWelcomeMessage = currentMessages.some(
|
| 208 |
+
(msg) => msg.id === expectedWelcomeId && msg.role === "assistant"
|
| 209 |
+
);
|
| 210 |
const modeChanged = prevChatModeRef.current !== chatMode;
|
| 211 |
|
| 212 |
if ((modeChanged || currentMessages.length === 0 || !hasWelcomeMessage) && !hasUserMessages) {
|
|
|
|
| 224 |
{
|
| 225 |
id: "review-1",
|
| 226 |
role: "assistant",
|
| 227 |
+
content:
|
| 228 |
+
"π Welcome to Review mode! I'll help you review and consolidate your learning. Let's go through what you've learned!",
|
| 229 |
timestamp: new Date(),
|
| 230 |
},
|
| 231 |
],
|
|
|
|
| 233 |
{
|
| 234 |
id: "quiz-1",
|
| 235 |
role: "assistant",
|
| 236 |
+
content:
|
| 237 |
+
"π― Welcome to Quiz mode! I'll test your understanding with personalized questions based on your learning history. Ready to start?",
|
| 238 |
timestamp: new Date(),
|
| 239 |
},
|
| 240 |
],
|
|
|
|
| 317 |
}
|
| 318 |
}, [user, groupMembers, availableCourses]);
|
| 319 |
|
|
|
|
| 320 |
const fallbackWorkspace: Workspace = {
|
| 321 |
id: "individual",
|
| 322 |
name: "My Space",
|
|
|
|
| 355 |
localStorage.setItem("theme", isDarkMode ? "dark" : "light");
|
| 356 |
}, [isDarkMode]);
|
| 357 |
|
| 358 |
+
// β
lock outer page scroll (defensive; your index.css already does this)
|
| 359 |
useEffect(() => {
|
| 360 |
const prev = document.body.style.overflow;
|
| 361 |
document.body.style.overflow = "hidden";
|
|
|
|
| 392 |
question: "Which of the following is NOT a principle of Responsible AI?",
|
| 393 |
options: ["A) Fairness", "B) Transparency", "C) Profit Maximization", "D) Accountability"],
|
| 394 |
correctAnswer: "C",
|
| 395 |
+
explanation:
|
| 396 |
+
"Profit Maximization is not a principle of Responsible AI. The key principles are Fairness, Transparency, and Accountability.",
|
| 397 |
},
|
| 398 |
{
|
| 399 |
type: "fill-in-blank",
|
| 400 |
+
question:
|
| 401 |
+
"Algorithmic fairness ensures that AI systems do not discriminate against individuals based on their _____.",
|
| 402 |
correctAnswer: "protected characteristics",
|
| 403 |
explanation:
|
| 404 |
"Protected characteristics include attributes like race, gender, age, religion, etc. AI systems should not discriminate based on these.",
|
|
|
|
| 584 |
setUploadedFiles((prev) => prev.filter((_, i) => i !== index));
|
| 585 |
};
|
| 586 |
|
|
|
|
| 587 |
const handleFileTypeChange = async (index: number, type: FileType) => {
|
| 588 |
if (!user) return;
|
| 589 |
|
|
|
|
| 620 |
|
| 621 |
return chat.messages.every((savedMsg, idx) => {
|
| 622 |
const currentMsg = messages[idx];
|
| 623 |
+
return (
|
| 624 |
+
savedMsg.id === currentMsg.id &&
|
| 625 |
+
savedMsg.role === currentMsg.role &&
|
| 626 |
+
savedMsg.content === currentMsg.content
|
| 627 |
+
);
|
| 628 |
});
|
| 629 |
}) || null
|
| 630 |
);
|
|
|
|
| 697 |
{
|
| 698 |
id: "review-1",
|
| 699 |
role: "assistant",
|
| 700 |
+
content:
|
| 701 |
+
"π Welcome to Review mode! I'll help you review and consolidate your learning. Let's go through what you've learned!",
|
| 702 |
timestamp: new Date(),
|
| 703 |
},
|
| 704 |
],
|
|
|
|
| 706 |
{
|
| 707 |
id: "quiz-1",
|
| 708 |
role: "assistant",
|
| 709 |
+
content:
|
| 710 |
+
"π― Welcome to Quiz mode! I'll test your understanding with personalized questions based on your learning history. Ready to start?",
|
| 711 |
timestamp: new Date(),
|
| 712 |
},
|
| 713 |
],
|
|
|
|
| 788 |
toast.success("Removed from saved items");
|
| 789 |
};
|
| 790 |
|
| 791 |
+
const handleCreateWorkspace = (payload: {
|
| 792 |
+
name: string;
|
| 793 |
+
category: "course" | "personal";
|
| 794 |
+
courseId?: string;
|
| 795 |
+
invites: string[];
|
| 796 |
+
}) => {
|
| 797 |
const id = `group-${Date.now()}`;
|
| 798 |
const avatar = `https://api.dicebear.com/7.x/shapes/svg?seed=${encodeURIComponent(payload.name)}`;
|
| 799 |
|
|
|
|
| 875 |
const handleOnboardingSkip = () => setShowOnboarding(false);
|
| 876 |
|
| 877 |
if (!user) return <LoginScreen onLogin={handleLogin} />;
|
| 878 |
+
if (showOnboarding && user)
|
| 879 |
+
return <Onboarding user={user} onComplete={handleOnboardingComplete} onSkip={handleOnboardingSkip} />;
|
| 880 |
|
| 881 |
return (
|
| 882 |
+
// β
Use fixed inset-0 instead of h-screen to avoid mobile/OS viewport quirks and ensure true viewport lock.
|
| 883 |
+
<div className="fixed inset-0 w-full bg-background overflow-hidden">
|
| 884 |
<Toaster />
|
| 885 |
|
| 886 |
+
{/* App vertical layout: header + (optional) banner + main */}
|
| 887 |
+
<div className="flex h-full min-h-0 min-w-0 flex-col overflow-hidden">
|
| 888 |
+
{/* Header (never scrolls) */}
|
| 889 |
<div className="flex-shrink-0">
|
| 890 |
<Header
|
| 891 |
user={user}
|
|
|
|
| 909 |
<ProfileEditor user={user} onSave={setUser} onClose={() => setShowProfileEditor(false)} />
|
| 910 |
)}
|
| 911 |
|
| 912 |
+
{/* Review banner (never scrolls) */}
|
| 913 |
{showReviewBanner && (
|
| 914 |
<div className="flex-shrink-0 w-full bg-background border-b border-border relative z-50">
|
| 915 |
<ReviewBanner onReview={handleReviewClick} onDismiss={handleDismissReviewBanner} />
|
| 916 |
</div>
|
| 917 |
)}
|
| 918 |
|
| 919 |
+
{/* Main row: ONLY children scroll; never this container */}
|
| 920 |
+
<div className="flex flex-1 min-h-0 min-w-0 overflow-hidden relative">
|
| 921 |
+
{/* Left panel open button (desktop) */}
|
| 922 |
{!leftPanelVisible && (
|
| 923 |
<Button
|
| 924 |
variant="secondary"
|
|
|
|
| 932 |
</Button>
|
| 933 |
)}
|
| 934 |
|
| 935 |
+
{/* Mobile overlay */}
|
| 936 |
{leftSidebarOpen && (
|
| 937 |
<div className="fixed inset-0 bg-black/50 z-40 lg:hidden" onClick={() => setLeftSidebarOpen(false)} />
|
| 938 |
)}
|
| 939 |
|
| 940 |
{/* Desktop left panel */}
|
| 941 |
{leftPanelVisible ? (
|
| 942 |
+
<aside className="hidden lg:flex w-80 h-full min-h-0 min-w-0 bg-card border-r border-border overflow-hidden relative flex-col">
|
| 943 |
<Button
|
| 944 |
variant="secondary"
|
| 945 |
size="icon"
|
|
|
|
| 951 |
<ChevronLeft className="h-3 w-3" />
|
| 952 |
</Button>
|
| 953 |
|
| 954 |
+
{/* IMPORTANT: do not add overflow-y here; let LeftSidebar decide its internal scrolling */}
|
| 955 |
<div className="flex-1 min-h-0 min-w-0 overflow-hidden">
|
| 956 |
<LeftSidebar
|
| 957 |
learningMode={learningMode}
|
|
|
|
| 982 |
</aside>
|
| 983 |
) : null}
|
| 984 |
|
| 985 |
+
{/* Mobile left drawer: height derives from viewport, not from header */}
|
| 986 |
<aside
|
| 987 |
+
className={[
|
| 988 |
+
"fixed lg:hidden z-50",
|
| 989 |
+
"left-0 top-0 bottom-0",
|
| 990 |
+
"w-80 bg-card border-r border-border",
|
| 991 |
+
"transform transition-transform duration-300 ease-in-out",
|
| 992 |
+
leftSidebarOpen ? "translate-x-0" : "-translate-x-full",
|
| 993 |
+
"overflow-hidden flex flex-col",
|
| 994 |
+
].join(" ")}
|
|
|
|
|
|
|
|
|
|
| 995 |
>
|
| 996 |
<div className="p-4 border-b border-border flex justify-between items-center flex-shrink-0">
|
| 997 |
<h3>Settings & Guide</h3>
|
|
|
|
| 1029 |
</div>
|
| 1030 |
</aside>
|
| 1031 |
|
| 1032 |
+
{/* Chat column: must be flex-col + min-h-0 + overflow-hidden so ChatArea can manage internal scroll */}
|
| 1033 |
+
<main className="flex flex-1 min-w-0 min-h-0 overflow-hidden flex-col">
|
| 1034 |
+
<div className="flex-1 min-h-0 min-w-0 overflow-hidden">
|
| 1035 |
<ChatArea
|
| 1036 |
messages={messages}
|
| 1037 |
onSendMessage={handleSendMessage}
|