Spaces:
Sleeping
Sleeping
Update web/src/App.tsx
Browse files- web/src/App.tsx +49 -15
web/src/App.tsx
CHANGED
|
@@ -1,5 +1,5 @@
|
|
| 1 |
// web/src/App.tsx
|
| 2 |
-
import React, { useState, useEffect, useRef } from "react";
|
| 3 |
import { Header } from "./components/Header";
|
| 4 |
import { LeftSidebar } from "./components/LeftSidebar";
|
| 5 |
import { ChatArea } from "./components/ChatArea";
|
|
@@ -15,6 +15,16 @@ import { toast } from "sonner";
|
|
| 15 |
// ✅ backend API bindings
|
| 16 |
import { apiChat, apiUpload, apiMemoryline } from "./lib/api";
|
| 17 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 18 |
export interface Message {
|
| 19 |
id: string;
|
| 20 |
role: "user" | "assistant";
|
|
@@ -204,9 +214,7 @@ function App() {
|
|
| 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) {
|
|
@@ -378,6 +386,35 @@ function App() {
|
|
| 378 |
})();
|
| 379 |
}, [user]);
|
| 380 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 381 |
const generateQuizQuestion = () => {
|
| 382 |
const questions: Array<{
|
| 383 |
type: "multiple-choice" | "fill-in-blank" | "open-ended";
|
|
@@ -879,13 +916,10 @@ function App() {
|
|
| 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}
|
|
@@ -902,6 +936,13 @@ function App() {
|
|
| 902 |
onLogout={() => setUser(null)}
|
| 903 |
availableCourses={availableCourses}
|
| 904 |
onUserUpdate={setUser}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 905 |
/>
|
| 906 |
</div>
|
| 907 |
|
|
@@ -909,16 +950,13 @@ function App() {
|
|
| 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,12 +970,10 @@ function App() {
|
|
| 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
|
|
@@ -951,7 +987,6 @@ function App() {
|
|
| 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,7 +1017,6 @@ function App() {
|
|
| 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",
|
|
@@ -1029,7 +1063,6 @@ function App() {
|
|
| 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
|
|
@@ -1069,6 +1102,7 @@ function App() {
|
|
| 1069 |
onCourseChange={setCurrentCourseId}
|
| 1070 |
availableCourses={availableCourses}
|
| 1071 |
showReviewBanner={showReviewBanner}
|
|
|
|
| 1072 |
/>
|
| 1073 |
</div>
|
| 1074 |
</main>
|
|
|
|
| 1 |
// web/src/App.tsx
|
| 2 |
+
import React, { useState, useEffect, useRef, useMemo } from "react";
|
| 3 |
import { Header } from "./components/Header";
|
| 4 |
import { LeftSidebar } from "./components/LeftSidebar";
|
| 5 |
import { ChatArea } from "./components/ChatArea";
|
|
|
|
| 15 |
// ✅ backend API bindings
|
| 16 |
import { apiChat, apiUpload, apiMemoryline } from "./lib/api";
|
| 17 |
|
| 18 |
+
// ✅ NEW: review-star logic
|
| 19 |
+
import {
|
| 20 |
+
type ReviewStarState,
|
| 21 |
+
type ReviewEventType,
|
| 22 |
+
markReviewActive,
|
| 23 |
+
normalizeToday,
|
| 24 |
+
starOpacity,
|
| 25 |
+
energyPct,
|
| 26 |
+
} from "./lib/reviewStar";
|
| 27 |
+
|
| 28 |
export interface Message {
|
| 29 |
id: string;
|
| 30 |
role: "user" | "assistant";
|
|
|
|
| 214 |
|
| 215 |
const hasUserMessages = currentMessages.some((msg) => msg.role === "user");
|
| 216 |
const expectedWelcomeId = chatMode === "ask" ? "1" : chatMode === "review" ? "review-1" : "quiz-1";
|
| 217 |
+
const hasWelcomeMessage = currentMessages.some((msg) => msg.id === expectedWelcomeId && msg.role === "assistant");
|
|
|
|
|
|
|
| 218 |
const modeChanged = prevChatModeRef.current !== chatMode;
|
| 219 |
|
| 220 |
if ((modeChanged || currentMessages.length === 0 || !hasWelcomeMessage) && !hasUserMessages) {
|
|
|
|
| 386 |
})();
|
| 387 |
}, [user]);
|
| 388 |
|
| 389 |
+
// =========================
|
| 390 |
+
// ✅ Review Star (按天) state
|
| 391 |
+
// scope:默认按 workspace 统计(你也可以改成 currentCourseId)
|
| 392 |
+
// =========================
|
| 393 |
+
const reviewStarKey = useMemo(() => {
|
| 394 |
+
if (!user) return "";
|
| 395 |
+
return `review_star::${user.email}::${currentWorkspaceId}`;
|
| 396 |
+
}, [user, currentWorkspaceId]);
|
| 397 |
+
|
| 398 |
+
const [reviewStarState, setReviewStarState] = useState<ReviewStarState | null>(null);
|
| 399 |
+
|
| 400 |
+
// 进入 Review tab 或 workspace 切换时:normalize(跨天归零 todayCount -> 星星自动变暗)
|
| 401 |
+
useEffect(() => {
|
| 402 |
+
if (!user || !reviewStarKey) return;
|
| 403 |
+
if (chatMode !== "review") return;
|
| 404 |
+
|
| 405 |
+
const next = normalizeToday(reviewStarKey);
|
| 406 |
+
setReviewStarState(next);
|
| 407 |
+
}, [chatMode, reviewStarKey, user]);
|
| 408 |
+
|
| 409 |
+
const handleReviewActivity = (event: ReviewEventType) => {
|
| 410 |
+
if (!user || !reviewStarKey) return;
|
| 411 |
+
const next = markReviewActive(reviewStarKey, event);
|
| 412 |
+
setReviewStarState(next);
|
| 413 |
+
};
|
| 414 |
+
|
| 415 |
+
const reviewStarOpacity = starOpacity(reviewStarState);
|
| 416 |
+
const reviewEnergyPct = energyPct(reviewStarState);
|
| 417 |
+
|
| 418 |
const generateQuizQuestion = () => {
|
| 419 |
const questions: Array<{
|
| 420 |
type: "multiple-choice" | "fill-in-blank" | "open-ended";
|
|
|
|
| 916 |
return <Onboarding user={user} onComplete={handleOnboardingComplete} onSkip={handleOnboardingSkip} />;
|
| 917 |
|
| 918 |
return (
|
|
|
|
| 919 |
<div className="fixed inset-0 w-full bg-background overflow-hidden">
|
| 920 |
<Toaster />
|
| 921 |
|
|
|
|
| 922 |
<div className="flex h-full min-h-0 min-w-0 flex-col overflow-hidden">
|
|
|
|
| 923 |
<div className="flex-shrink-0">
|
| 924 |
<Header
|
| 925 |
user={user}
|
|
|
|
| 936 |
onLogout={() => setUser(null)}
|
| 937 |
availableCourses={availableCourses}
|
| 938 |
onUserUpdate={setUser}
|
| 939 |
+
reviewStarOpacity={reviewStarOpacity}
|
| 940 |
+
reviewEnergyPct={reviewEnergyPct}
|
| 941 |
+
onStarClick={() => {
|
| 942 |
+
setChatMode("review");
|
| 943 |
+
setShowReviewBanner(false);
|
| 944 |
+
localStorage.setItem("reviewBannerDismissed", "true");
|
| 945 |
+
}}
|
| 946 |
/>
|
| 947 |
</div>
|
| 948 |
|
|
|
|
| 950 |
<ProfileEditor user={user} onSave={setUser} onClose={() => setShowProfileEditor(false)} />
|
| 951 |
)}
|
| 952 |
|
|
|
|
| 953 |
{showReviewBanner && (
|
| 954 |
<div className="flex-shrink-0 w-full bg-background border-b border-border relative z-50">
|
| 955 |
<ReviewBanner onReview={handleReviewClick} onDismiss={handleDismissReviewBanner} />
|
| 956 |
</div>
|
| 957 |
)}
|
| 958 |
|
|
|
|
| 959 |
<div className="flex flex-1 min-h-0 min-w-0 overflow-hidden relative">
|
|
|
|
| 960 |
{!leftPanelVisible && (
|
| 961 |
<Button
|
| 962 |
variant="secondary"
|
|
|
|
| 970 |
</Button>
|
| 971 |
)}
|
| 972 |
|
|
|
|
| 973 |
{leftSidebarOpen && (
|
| 974 |
<div className="fixed inset-0 bg-black/50 z-40 lg:hidden" onClick={() => setLeftSidebarOpen(false)} />
|
| 975 |
)}
|
| 976 |
|
|
|
|
| 977 |
{leftPanelVisible ? (
|
| 978 |
<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">
|
| 979 |
<Button
|
|
|
|
| 987 |
<ChevronLeft className="h-3 w-3" />
|
| 988 |
</Button>
|
| 989 |
|
|
|
|
| 990 |
<div className="flex-1 min-h-0 min-w-0 overflow-hidden">
|
| 991 |
<LeftSidebar
|
| 992 |
learningMode={learningMode}
|
|
|
|
| 1017 |
</aside>
|
| 1018 |
) : null}
|
| 1019 |
|
|
|
|
| 1020 |
<aside
|
| 1021 |
className={[
|
| 1022 |
"fixed lg:hidden z-50",
|
|
|
|
| 1063 |
</div>
|
| 1064 |
</aside>
|
| 1065 |
|
|
|
|
| 1066 |
<main className="flex flex-1 min-w-0 min-h-0 overflow-hidden flex-col">
|
| 1067 |
<div className="flex-1 min-h-0 min-w-0 overflow-hidden">
|
| 1068 |
<ChatArea
|
|
|
|
| 1102 |
onCourseChange={setCurrentCourseId}
|
| 1103 |
availableCourses={availableCourses}
|
| 1104 |
showReviewBanner={showReviewBanner}
|
| 1105 |
+
onReviewActivity={handleReviewActivity}
|
| 1106 |
/>
|
| 1107 |
</div>
|
| 1108 |
</main>
|