SarahXia0405 commited on
Commit
6a7c6e8
·
verified ·
1 Parent(s): 6e1c816

Update web/src/components/ChatArea.tsx

Browse files
Files changed (1) hide show
  1. web/src/components/ChatArea.tsx +128 -155
web/src/components/ChatArea.tsx CHANGED
@@ -137,7 +137,7 @@ export function ChatArea({
137
  workspaces,
138
  currentWorkspaceId,
139
  onSaveFile,
140
- leftPanelVisible = false, // kept for props compatibility
141
  currentCourseId,
142
  onCourseChange,
143
  availableCourses = [],
@@ -176,8 +176,7 @@ export function ChatArea({
176
  { id: "course4", name: "Web Development" },
177
  ];
178
 
179
- // IMPORTANT:
180
- // messagesEndRef is used to scroll inside the scroll container
181
  const messagesEndRef = useRef<HTMLDivElement>(null);
182
  const scrollContainerRef = useRef<HTMLDivElement>(null);
183
  const fileInputRef = useRef<HTMLInputElement>(null);
@@ -186,16 +185,13 @@ export function ChatArea({
186
  const previousMessagesLength = useRef(messages.length);
187
 
188
  const scrollToBottom = () => {
189
- // scrollIntoView works as long as messagesEndRef is inside scrollContainerRef
190
  messagesEndRef.current?.scrollIntoView({ behavior: "smooth", block: "end" });
191
  };
192
 
193
- // Only auto-scroll when new messages are added (not on initial load)
194
  useEffect(() => {
195
  if (isInitialMount.current) {
196
  isInitialMount.current = false;
197
  previousMessagesLength.current = messages.length;
198
-
199
  // Keep at top on initial load (your old behavior)
200
  if (scrollContainerRef.current) {
201
  scrollContainerRef.current.scrollTop = 0;
@@ -209,7 +205,6 @@ export function ChatArea({
209
  previousMessagesLength.current = messages.length;
210
  }, [messages]);
211
 
212
- // Scroll button + top border
213
  useEffect(() => {
214
  const handleScroll = () => {
215
  const el = scrollContainerRef.current;
@@ -253,7 +248,6 @@ export function ChatArea({
253
  summary: "Quick Summary",
254
  };
255
 
256
- // Review topic click
257
  const handleReviewTopic = (item: {
258
  title: string;
259
  previousQuestion: string;
@@ -365,7 +359,6 @@ export function ChatArea({
365
  }
366
  };
367
 
368
- // Check if current chat is already saved
369
  const isCurrentChatSaved = (): boolean => {
370
  if (messages.length <= 1) return false;
371
 
@@ -525,7 +518,6 @@ export function ChatArea({
525
  setPendingFiles((prev) => prev.map((pf, i) => (i === index ? { ...pf, type } : pf)));
526
  };
527
 
528
- // File helpers
529
  const getFileIcon = (filename: string) => {
530
  const ext = filename.toLowerCase();
531
  if (ext.endsWith(".pdf")) return FileText;
@@ -737,147 +729,141 @@ export function ChatArea({
737
  );
738
  };
739
 
740
- // ==============
741
- // Layout constants
742
- // ==============
743
- // This reserves enough space so the last message won't be hidden behind the bottom composer.
744
- // If your composer grows, bump this value.
745
- const MESSAGES_BOTTOM_PADDING = "13rem";
746
 
747
  return (
748
- // IMPORTANT: This component must be a column with NO outer scroll.
749
  <div className="relative flex flex-col h-full min-h-0 overflow-hidden">
750
  {/* =========================
751
- 1) Scroll Container (ONLY this area scrolls)
752
  ========================= */}
753
  <div
754
- ref={scrollContainerRef}
755
- className="flex-1 min-h-0 overflow-y-auto"
756
- style={{ overscrollBehavior: "contain" }}
 
757
  >
758
- {/* Top Bar - Sticky (stays at top inside the scroll container) */}
759
- <div
760
- className={`sticky top-0 flex items-center justify-between px-4 z-20 bg-card ${
761
- showTopBorder ? "border-b border-border" : ""
762
- }`}
763
- style={{ height: "4.5rem", margin: 0, padding: "1rem 1rem", boxSizing: "border-box" }}
764
- >
765
- {/* Course Selector - Left */}
766
- <div className="flex-shrink-0">
767
- {(() => {
768
- const current = workspaces.find((w) => w.id === currentWorkspaceId);
769
- if (current?.type === "group") {
770
- if (current.category === "course" && current.courseName) {
771
- return (
772
- <div className="h-9 px-3 inline-flex items-center rounded-md border font-semibold">
773
- {current.courseName}
774
- </div>
775
- );
776
- }
777
- return null;
778
  }
 
 
779
 
780
- return (
781
- <Select
782
- value={currentCourseId || "course1"}
783
- onValueChange={(val) => onCourseChange && onCourseChange(val)}
784
- >
785
- <SelectTrigger className="w-[200px] h-9 font-semibold">
786
- <SelectValue placeholder="Select course" />
787
- </SelectTrigger>
788
- <SelectContent>
789
- {courses.map((course) => (
790
- <SelectItem key={course.id} value={course.id}>
791
- {course.name}
792
- </SelectItem>
793
- ))}
794
- </SelectContent>
795
- </Select>
796
- );
797
- })()}
798
- </div>
799
 
800
- {/* Chat Mode Tabs - Center */}
801
- <div className="absolute left-1/2 -translate-x-1/2 flex-shrink-0">
802
- <Tabs
803
- value={chatMode}
804
- onValueChange={(value) => onChatModeChange(value as ChatMode)}
805
- className="w-auto"
806
- orientation="horizontal"
807
- >
808
- <TabsList className="inline-flex h-8 items-center justify-center rounded-xl bg-muted p-1 text-muted-foreground">
809
- <TabsTrigger value="ask" className="w-[140px] px-3 text-sm">
810
- Ask
811
- </TabsTrigger>
812
- <TabsTrigger value="review" className="w-[140px] px-3 text-sm relative">
813
- Review
814
- <span
815
- className="absolute top-0 right-0 bg-red-500 rounded-full border-2"
816
- style={{
817
- width: "10px",
818
- height: "10px",
819
- transform: "translate(25%, -25%)",
820
- zIndex: 10,
821
- borderColor: "var(--muted)",
822
- }}
823
- />
824
- </TabsTrigger>
825
- <TabsTrigger value="quiz" className="w-[140px] px-3 text-sm">
826
- Quiz
827
- </TabsTrigger>
828
- </TabsList>
829
- </Tabs>
830
- </div>
831
 
832
- {/* Action Buttons - Right */}
833
- <div className="flex items-center gap-2 flex-shrink-0">
834
- <Button
835
- variant="ghost"
836
- size="icon"
837
- onClick={handleSaveClick}
838
- disabled={!isLoggedIn}
839
- className={`h-8 w-8 rounded-md hover:bg-muted/50 ${isCurrentChatSaved() ? "text-primary" : ""}`}
840
- title={isCurrentChatSaved() ? "Unsave" : "Save"}
841
- >
842
- <Bookmark className={`h-4 w-4 ${isCurrentChatSaved() ? "fill-primary text-primary" : ""}`} />
843
- </Button>
844
 
845
- <Button
846
- variant="ghost"
847
- size="icon"
848
- onClick={handleOpenDownloadDialog}
849
- disabled={!isLoggedIn}
850
- className="h-8 w-8 rounded-md hover:bg-muted/50"
851
- title="Download"
852
- >
853
- <Download className="h-4 w-4" />
854
- </Button>
855
 
856
- <Button
857
- variant="ghost"
858
- size="icon"
859
- onClick={handleShareClick}
860
- disabled={!isLoggedIn}
861
- className="h-8 w-8 rounded-md hover:bg-muted/50"
862
- title="Share"
863
- >
864
- <Share2 className="h-4 w-4" />
865
- </Button>
866
 
867
- <Button
868
- variant="outline"
869
- onClick={handleClearClick}
870
- disabled={!isLoggedIn}
871
- className="h-8 px-3 gap-2 rounded-md border border-border disabled:opacity-60 !bg-[var(--card)] !text-[var(--card-foreground)] hover:!opacity-90 [&_svg]:!text-[var(--card-foreground)] [&_span]:!text-[var(--card-foreground)]"
872
- title="New Chat"
873
- >
874
- <Plus className="h-4 w-4" />
875
- <span className="text-sm font-medium">New chat</span>
876
- </Button>
877
- </div>
878
  </div>
 
879
 
880
- {/* Messages Content */}
 
 
 
 
 
 
 
881
  <div className="py-6" style={{ paddingBottom: MESSAGES_BOTTOM_PADDING }}>
882
  <div className="w-full space-y-6 max-w-4xl mx-auto">
883
  {messages.map((message) => (
@@ -925,31 +911,21 @@ export function ChatArea({
925
  </div>
926
  <div className="bg-muted rounded-2xl px-4 py-3">
927
  <div className="flex gap-1">
928
- <div
929
- className="w-2 h-2 rounded-full bg-muted-foreground/50 animate-bounce"
930
- style={{ animationDelay: "0ms" }}
931
- />
932
- <div
933
- className="w-2 h-2 rounded-full bg-muted-foreground/50 animate-bounce"
934
- style={{ animationDelay: "150ms" }}
935
- />
936
- <div
937
- className="w-2 h-2 rounded-full bg-muted-foreground/50 animate-bounce"
938
- style={{ animationDelay: "300ms" }}
939
- />
940
  </div>
941
  </div>
942
  </div>
943
  )}
944
 
945
- {/* MUST be inside scroll container */}
946
  <div ref={messagesEndRef} />
947
  </div>
948
  </div>
949
  </div>
950
 
951
  {/* =========================
952
- 2) Scroll-to-bottom button (fixed relative to ChatArea, NOT inside scroll)
953
  ========================= */}
954
  {showScrollButton && (
955
  <div className="absolute z-30 left-0 right-0 flex justify-center pointer-events-none" style={{ bottom: "132px" }}>
@@ -966,10 +942,9 @@ export function ChatArea({
966
  )}
967
 
968
  {/* =========================
969
- 3) Composer (pinned to bottom of ChatArea)
970
- NOTE: moved from absolute -> sticky footer container.
971
  ========================= */}
972
- <div className="flex-shrink-0 sticky bottom-0 left-0 right-0 bg-background/95 backdrop-blur-sm z-20 border-t border-border">
973
  <div className="max-w-4xl mx-auto px-4 py-4">
974
  {uploadedFiles.length > 0 && (
975
  <div className="mb-2 flex flex-wrap gap-2 max-h-32 overflow-y-auto">
@@ -1349,9 +1324,7 @@ export function ChatArea({
1349
  </DialogTitle>
1350
  <DialogDescription>File size: {selectedFile ? formatFileSize(selectedFile.file.size) : ""}</DialogDescription>
1351
  </DialogHeader>
1352
- <div className="flex-1 min-h-0 overflow-y-auto mt-4">
1353
- {selectedFile && <FileViewerContent file={selectedFile.file} />}
1354
- </div>
1355
  </DialogContent>
1356
  </Dialog>
1357
 
 
137
  workspaces,
138
  currentWorkspaceId,
139
  onSaveFile,
140
+ leftPanelVisible = false,
141
  currentCourseId,
142
  onCourseChange,
143
  availableCourses = [],
 
176
  { id: "course4", name: "Web Development" },
177
  ];
178
 
179
+ // Messages scroll container
 
180
  const messagesEndRef = useRef<HTMLDivElement>(null);
181
  const scrollContainerRef = useRef<HTMLDivElement>(null);
182
  const fileInputRef = useRef<HTMLInputElement>(null);
 
185
  const previousMessagesLength = useRef(messages.length);
186
 
187
  const scrollToBottom = () => {
 
188
  messagesEndRef.current?.scrollIntoView({ behavior: "smooth", block: "end" });
189
  };
190
 
 
191
  useEffect(() => {
192
  if (isInitialMount.current) {
193
  isInitialMount.current = false;
194
  previousMessagesLength.current = messages.length;
 
195
  // Keep at top on initial load (your old behavior)
196
  if (scrollContainerRef.current) {
197
  scrollContainerRef.current.scrollTop = 0;
 
205
  previousMessagesLength.current = messages.length;
206
  }, [messages]);
207
 
 
208
  useEffect(() => {
209
  const handleScroll = () => {
210
  const el = scrollContainerRef.current;
 
248
  summary: "Quick Summary",
249
  };
250
 
 
251
  const handleReviewTopic = (item: {
252
  title: string;
253
  previousQuestion: string;
 
359
  }
360
  };
361
 
 
362
  const isCurrentChatSaved = (): boolean => {
363
  if (messages.length <= 1) return false;
364
 
 
518
  setPendingFiles((prev) => prev.map((pf, i) => (i === index ? { ...pf, type } : pf)));
519
  };
520
 
 
521
  const getFileIcon = (filename: string) => {
522
  const ext = filename.toLowerCase();
523
  if (ext.endsWith(".pdf")) return FileText;
 
729
  );
730
  };
731
 
732
+ // now composer is fixed as sibling => no huge bottom padding needed
733
+ const MESSAGES_BOTTOM_PADDING = "2rem";
 
 
 
 
734
 
735
  return (
736
+ // Three-part layout: TopBar (fixed) + Messages (scroll) + Composer (fixed)
737
  <div className="relative flex flex-col h-full min-h-0 overflow-hidden">
738
  {/* =========================
739
+ 1) TOP BAR (fixed)
740
  ========================= */}
741
  <div
742
+ className={`flex-shrink-0 flex items-center justify-between px-4 bg-card ${
743
+ showTopBorder ? "border-b border-border" : "border-b border-border"
744
+ }`}
745
+ style={{ height: "4.5rem", margin: 0, padding: "1rem 1rem", boxSizing: "border-box" }}
746
  >
747
+ {/* Course Selector - Left */}
748
+ <div className="flex-shrink-0">
749
+ {(() => {
750
+ const current = workspaces.find((w) => w.id === currentWorkspaceId);
751
+ if (current?.type === "group") {
752
+ if (current.category === "course" && current.courseName) {
753
+ return (
754
+ <div className="h-9 px-3 inline-flex items-center rounded-md border font-semibold">
755
+ {current.courseName}
756
+ </div>
757
+ );
 
 
 
 
 
 
 
 
 
758
  }
759
+ return null;
760
+ }
761
 
762
+ return (
763
+ <Select value={currentCourseId || "course1"} onValueChange={(val) => onCourseChange && onCourseChange(val)}>
764
+ <SelectTrigger className="w-[200px] h-9 font-semibold">
765
+ <SelectValue placeholder="Select course" />
766
+ </SelectTrigger>
767
+ <SelectContent>
768
+ {courses.map((course) => (
769
+ <SelectItem key={course.id} value={course.id}>
770
+ {course.name}
771
+ </SelectItem>
772
+ ))}
773
+ </SelectContent>
774
+ </Select>
775
+ );
776
+ })()}
777
+ </div>
 
 
 
778
 
779
+ {/* Chat Mode Tabs - Center */}
780
+ <div className="absolute left-1/2 -translate-x-1/2 flex-shrink-0">
781
+ <Tabs
782
+ value={chatMode}
783
+ onValueChange={(value) => onChatModeChange(value as ChatMode)}
784
+ className="w-auto"
785
+ orientation="horizontal"
786
+ >
787
+ <TabsList className="inline-flex h-8 items-center justify-center rounded-xl bg-muted p-1 text-muted-foreground">
788
+ <TabsTrigger value="ask" className="w-[140px] px-3 text-sm">
789
+ Ask
790
+ </TabsTrigger>
791
+ <TabsTrigger value="review" className="w-[140px] px-3 text-sm relative">
792
+ Review
793
+ <span
794
+ className="absolute top-0 right-0 bg-red-500 rounded-full border-2"
795
+ style={{
796
+ width: "10px",
797
+ height: "10px",
798
+ transform: "translate(25%, -25%)",
799
+ zIndex: 10,
800
+ borderColor: "var(--muted)",
801
+ }}
802
+ />
803
+ </TabsTrigger>
804
+ <TabsTrigger value="quiz" className="w-[140px] px-3 text-sm">
805
+ Quiz
806
+ </TabsTrigger>
807
+ </TabsList>
808
+ </Tabs>
809
+ </div>
810
 
811
+ {/* Action Buttons - Right */}
812
+ <div className="flex items-center gap-2 flex-shrink-0">
813
+ <Button
814
+ variant="ghost"
815
+ size="icon"
816
+ onClick={handleSaveClick}
817
+ disabled={!isLoggedIn}
818
+ className={`h-8 w-8 rounded-md hover:bg-muted/50 ${isCurrentChatSaved() ? "text-primary" : ""}`}
819
+ title={isCurrentChatSaved() ? "Unsave" : "Save"}
820
+ >
821
+ <Bookmark className={`h-4 w-4 ${isCurrentChatSaved() ? "fill-primary text-primary" : ""}`} />
822
+ </Button>
823
 
824
+ <Button
825
+ variant="ghost"
826
+ size="icon"
827
+ onClick={handleOpenDownloadDialog}
828
+ disabled={!isLoggedIn}
829
+ className="h-8 w-8 rounded-md hover:bg-muted/50"
830
+ title="Download"
831
+ >
832
+ <Download className="h-4 w-4" />
833
+ </Button>
834
 
835
+ <Button
836
+ variant="ghost"
837
+ size="icon"
838
+ onClick={handleShareClick}
839
+ disabled={!isLoggedIn}
840
+ className="h-8 w-8 rounded-md hover:bg-muted/50"
841
+ title="Share"
842
+ >
843
+ <Share2 className="h-4 w-4" />
844
+ </Button>
845
 
846
+ <Button
847
+ variant="outline"
848
+ onClick={handleClearClick}
849
+ disabled={!isLoggedIn}
850
+ className="h-8 px-3 gap-2 rounded-md border border-border disabled:opacity-60 !bg-[var(--card)] !text-[var(--card-foreground)] hover:!opacity-90 [&_svg]:!text-[var(--card-foreground)] [&_span]:!text-[var(--card-foreground)]"
851
+ title="New Chat"
852
+ >
853
+ <Plus className="h-4 w-4" />
854
+ <span className="text-sm font-medium">New chat</span>
855
+ </Button>
 
856
  </div>
857
+ </div>
858
 
859
+ {/* =========================
860
+ 2) MESSAGES (ONLY this area scrolls)
861
+ ========================= */}
862
+ <div
863
+ ref={scrollContainerRef}
864
+ className="flex-1 min-h-0 overflow-y-auto"
865
+ style={{ overscrollBehavior: "contain" }}
866
+ >
867
  <div className="py-6" style={{ paddingBottom: MESSAGES_BOTTOM_PADDING }}>
868
  <div className="w-full space-y-6 max-w-4xl mx-auto">
869
  {messages.map((message) => (
 
911
  </div>
912
  <div className="bg-muted rounded-2xl px-4 py-3">
913
  <div className="flex gap-1">
914
+ <div className="w-2 h-2 rounded-full bg-muted-foreground/50 animate-bounce" style={{ animationDelay: "0ms" }} />
915
+ <div className="w-2 h-2 rounded-full bg-muted-foreground/50 animate-bounce" style={{ animationDelay: "150ms" }} />
916
+ <div className="w-2 h-2 rounded-full bg-muted-foreground/50 animate-bounce" style={{ animationDelay: "300ms" }} />
 
 
 
 
 
 
 
 
 
917
  </div>
918
  </div>
919
  </div>
920
  )}
921
 
 
922
  <div ref={messagesEndRef} />
923
  </div>
924
  </div>
925
  </div>
926
 
927
  {/* =========================
928
+ 2.5) Scroll-to-bottom button (fixed)
929
  ========================= */}
930
  {showScrollButton && (
931
  <div className="absolute z-30 left-0 right-0 flex justify-center pointer-events-none" style={{ bottom: "132px" }}>
 
942
  )}
943
 
944
  {/* =========================
945
+ 3) COMPOSER (fixed bottom, never scroll)
 
946
  ========================= */}
947
+ <div className="flex-shrink-0 bg-background/95 backdrop-blur-sm z-20 border-t border-border">
948
  <div className="max-w-4xl mx-auto px-4 py-4">
949
  {uploadedFiles.length > 0 && (
950
  <div className="mb-2 flex flex-wrap gap-2 max-h-32 overflow-y-auto">
 
1324
  </DialogTitle>
1325
  <DialogDescription>File size: {selectedFile ? formatFileSize(selectedFile.file.size) : ""}</DialogDescription>
1326
  </DialogHeader>
1327
+ <div className="flex-1 min-h-0 overflow-y-auto mt-4">{selectedFile && <FileViewerContent file={selectedFile.file} />}</div>
 
 
1328
  </DialogContent>
1329
  </Dialog>
1330