SarahXia0405 commited on
Commit
5072b2a
·
verified ·
1 Parent(s): b180cdf

Update web/src/App.tsx

Browse files
Files changed (1) hide show
  1. 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>