SarahXia0405 commited on
Commit
c4a3076
·
verified ·
1 Parent(s): 24c1d05

Update web/src/components/ChatArea.tsx

Browse files
Files changed (1) hide show
  1. web/src/components/ChatArea.tsx +100 -193
web/src/components/ChatArea.tsx CHANGED
@@ -50,6 +50,9 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from ".
50
  import { SmartReview } from "./SmartReview";
51
  import clareAvatar from "../assets/dfe44dab3ad8cd93953eac4a3e68bd1a5f999653.png";
52
 
 
 
 
53
  type ReviewEventType = "send_message" | "review_topic" | "review_all";
54
 
55
  interface ChatAreaProps {
@@ -89,7 +92,6 @@ interface ChatAreaProps {
89
  availableCourses?: Array<{ id: string; name: string }>;
90
  showReviewBanner?: boolean;
91
 
92
- // ✅ NEW: for Review star brightness (daily)
93
  onReviewActivity?: (event: ReviewEventType) => void;
94
  }
95
 
@@ -199,17 +201,14 @@ export function ChatArea({
199
  return;
200
  }
201
 
202
- // smooth scroll without scrollIntoView (prevents scrolling the wrong ancestor)
203
  el.scrollTo({ top, behavior });
204
  };
205
 
206
- // Auto-scroll only when new messages come in (not on initial mount)
207
  useEffect(() => {
208
  if (isInitialMount.current) {
209
  isInitialMount.current = false;
210
  previousMessagesLength.current = messages.length;
211
 
212
- // Keep your previous behavior: start at top
213
  const el = scrollContainerRef.current;
214
  if (el) el.scrollTop = 0;
215
  return;
@@ -225,7 +224,6 @@ export function ChatArea({
225
  previousMessagesLength.current = messages.length;
226
  }, [messages]);
227
 
228
- // Scroll button + top border
229
  useEffect(() => {
230
  const container = scrollContainerRef.current;
231
  if (!container) return;
@@ -246,7 +244,6 @@ export function ChatArea({
246
  e.preventDefault();
247
  if (!input.trim() || !isLoggedIn) return;
248
 
249
- // ✅ Review activity: user sent a message in Review tab
250
  if (chatMode === "review") onReviewActivity?.("send_message");
251
 
252
  onSendMessage(input);
@@ -269,7 +266,6 @@ export function ChatArea({
269
  summary: "Quick Summary",
270
  };
271
 
272
- // Review topic click
273
  const handleReviewTopic = (item: {
274
  title: string;
275
  previousQuestion: string;
@@ -279,7 +275,6 @@ export function ChatArea({
279
  weight: number;
280
  lastReviewed: string;
281
  }) => {
282
- // ✅ Review activity: clicked Review this topic
283
  onReviewActivity?.("review_topic");
284
 
285
  const userMessage = `Please help me review: ${item.title}`;
@@ -289,9 +284,7 @@ export function ChatArea({
289
  };
290
 
291
  const handleReviewAll = () => {
292
- // ✅ Review activity: clicked Review all
293
  onReviewActivity?.("review_all");
294
-
295
  (window as any).__lastReviewData = "REVIEW_ALL";
296
  onSendMessage("Please help me review all topics that need attention.");
297
  };
@@ -385,7 +378,6 @@ export function ChatArea({
385
  }
386
  };
387
 
388
- // Check if current chat is already saved
389
  const isCurrentChatSaved = (): boolean => {
390
  if (messages.length <= 1) return false;
391
 
@@ -441,7 +433,7 @@ export function ChatArea({
441
  const saved = isCurrentChatSaved();
442
 
443
  if (saved) {
444
- onConfirmClear(false);
445
  return;
446
  }
447
 
@@ -567,114 +559,87 @@ export function ChatArea({
567
  return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
568
  };
569
 
570
- // ✅ CHANGE 1: onRemove 改成无参,避免事件传递导致“点了没反应”
571
- const FileThumbnail = ({
 
 
 
 
 
 
 
 
 
572
  file,
573
- Icon,
574
- fileInfo,
575
- isImage,
576
- onPreview,
577
- onRemove,
578
  }: {
579
  file: File;
580
- Icon: React.ComponentType<{ className?: string }>;
581
- fileInfo: { bgColor: string; type: string };
582
- isImage: boolean;
583
- onPreview: () => void;
584
- onRemove: () => void; // ✅ CHANGED
585
  }) => {
586
- const [imagePreview, setImagePreview] = useState<string | null>(null);
587
- const [imageLoading, setImageLoading] = useState(true);
588
 
589
- useEffect(() => {
590
- if (!isImage) {
591
- setImagePreview(null);
592
- setImageLoading(false);
593
- return;
594
- }
 
 
 
595
 
596
- setImageLoading(true);
597
- const reader = new FileReader();
598
- reader.onload = (e) => {
599
- setImagePreview(e.target?.result as string);
600
- setImageLoading(false);
601
- };
602
- reader.onerror = () => setImageLoading(false);
603
- reader.readAsDataURL(file);
604
- }, [file, isImage]);
605
 
606
- if (isImage) {
607
- return (
608
- <div
609
- className="relative cursor-pointer w-16 h-16 flex-shrink-0"
610
- onClick={onPreview}
611
- style={{ width: 64, height: 64 }}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
612
  >
613
- <div className="w-full h-full relative bg-card border border-border rounded-lg hover:border-primary/50 transition-colors">
614
- <div className="w-full h-full overflow-hidden rounded-lg absolute inset-0">
615
- {imageLoading ? (
616
- <div className="w-full h-full flex items-center justify-center bg-muted">
617
- <Icon className="h-5 w-5 text-muted-foreground animate-pulse" />
618
- </div>
619
- ) : imagePreview ? (
620
- <img
621
- src={imagePreview}
622
- alt={file.name}
623
- className="w-full h-full object-cover"
624
- onError={(e) => {
625
- e.currentTarget.style.display = "none";
626
- setImageLoading(false);
627
- }}
628
- />
629
- ) : (
630
- <div className="w-full h-full flex items-center justify-center bg-muted">
631
- <Icon className="h-5 w-5 text-muted-foreground" />
632
- </div>
633
- )}
634
- </div>
635
 
636
- <button
637
- type="button"
638
- className="absolute top-1 right-1 h-4 w-4 rounded-full bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 shadow-sm flex items-center justify-center cursor-pointer"
639
- onClick={(e) => {
640
- e.stopPropagation();
641
- onRemove(); // ✅ CHANGED
642
- }}
643
- style={{ zIndex: 100 }}
644
- >
645
- <X className="h-2.5 w-2.5" style={{ color: "rgb(0, 0, 0)", strokeWidth: 2 }} />
646
- </button>
647
  </div>
 
648
  </div>
649
- );
650
- }
651
 
652
- return (
653
- <div className="relative cursor-pointer" onClick={onPreview} style={{ width: 240, flexShrink: 0 }}>
654
- <div className="h-16 w-full relative flex items-center px-3 bg-card border border-border rounded-lg hover:border-primary/50 transition-colors">
655
- <div className={`${fileInfo.bgColor} flex items-center justify-center w-10 h-10 rounded shrink-0`}>
656
- <Icon className="h-5 w-5 text-white" />
657
- </div>
658
- <div className="w-2 shrink-0" />
659
- <div className="flex-1 min-w-0 flex flex-col justify-center pr-7">
660
- <p className="text-xs text-foreground truncate" title={file.name}>
661
- {file.name}
662
- </p>
663
- <p className="text-[10px] text-muted-foreground mt-0.5 truncate">{fileInfo.type}</p>
 
 
664
  </div>
665
-
666
- <button
667
- type="button"
668
- className="absolute top-1 right-1 h-4 w-4 rounded-full bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 shadow-sm flex items-center justify-center cursor-pointer z-10"
669
- onClick={(e) => {
670
- e.stopPropagation();
671
- onRemove(); // ✅ CHANGED
672
- }}
673
- style={{ zIndex: 10 }}
674
- >
675
- <X className="h-2.5 w-2.5" style={{ color: "rgb(0, 0, 0)", strokeWidth: 2 }} />
676
- </button>
677
- </div>
678
  </div>
679
  );
680
  };
@@ -693,16 +658,9 @@ export function ChatArea({
693
  const ext = file.name.toLowerCase();
694
 
695
  if ([".jpg", ".jpeg", ".png", ".gif", ".webp"].some((e) => ext.endsWith(e))) {
696
- const reader = new FileReader();
697
- reader.onload = (e) => {
698
- setContent(e.target?.result as string);
699
- setLoading(false);
700
- };
701
- reader.onerror = () => {
702
- setError("Failed to load image");
703
- setLoading(false);
704
- };
705
- reader.readAsDataURL(file);
706
  return;
707
  }
708
 
@@ -729,6 +687,7 @@ export function ChatArea({
729
  };
730
 
731
  loadFile();
 
732
  }, [file]);
733
 
734
  if (loading) return <div className="text-center py-8">Loading...</div>;
@@ -745,17 +704,18 @@ export function ChatArea({
745
  );
746
  }
747
 
748
- return <div className="whitespace-pre-wrap text-sm font-mono p-4 bg-muted rounded-lg max-h-[60vh] overflow-y-auto">{content}</div>;
 
 
 
 
749
  };
750
 
751
- // ✅ Reserve space for composer so last message is never hidden
752
  const bottomPad = Math.max(24, composerHeight + 24);
753
 
754
  return (
755
  <div className="relative flex flex-col h-full min-h-0 w-full overflow-hidden">
756
- {/* =========================
757
- 0) Top Bar (fixed in ChatArea layout; NOT inside scroll container)
758
- ========================= */}
759
  <div
760
  className={`flex-shrink-0 flex items-center justify-between px-4 bg-card z-20 ${
761
  showTopBorder ? "border-b border-border" : ""
@@ -768,11 +728,7 @@ export function ChatArea({
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
  }
@@ -794,7 +750,7 @@ export function ChatArea({
794
  })()}
795
  </div>
796
 
797
- {/* Chat Mode Tabs - Center */}
798
  <div className="absolute left-1/2 -translate-x-1/2 flex-shrink-0">
799
  <Tabs value={chatMode} onValueChange={(value) => onChatModeChange(value as ChatMode)} className="w-auto" orientation="horizontal">
800
  <TabsList className="inline-flex h-8 items-center justify-center rounded-xl bg-muted p-1 text-muted-foreground">
@@ -869,11 +825,8 @@ export function ChatArea({
869
  </div>
870
  </div>
871
 
872
- {/* =========================
873
- 1) Scroll Container (ONLY this scrolls)
874
- ========================= */}
875
  <div ref={scrollContainerRef} className="flex-1 min-h-0 overflow-y-auto overscroll-contain" style={{ overscrollBehavior: "contain" }}>
876
- {/* Messages */}
877
  <div className="py-6" style={{ paddingBottom: bottomPad }}>
878
  <div className="w-full space-y-6 max-w-4xl mx-auto">
879
  {messages.map((message) => (
@@ -881,9 +834,7 @@ export function ChatArea({
881
  <Message
882
  message={message}
883
  showSenderInfo={spaceType === "group"}
884
- isFirstGreeting={
885
- (message.id === "1" || message.id === "review-1" || message.id === "quiz-1") && message.role === "assistant"
886
- }
887
  showNextButton={message.showNextButton && !isAppTyping}
888
  onNextQuestion={onNextQuestion}
889
  chatMode={chatMode}
@@ -931,9 +882,7 @@ export function ChatArea({
931
  </div>
932
  </div>
933
 
934
- {/* =========================
935
- 2) Scroll-to-bottom button (positioned above composer)
936
- ========================= */}
937
  {showScrollButton && (
938
  <div className="absolute z-30 left-0 right-0 flex justify-center pointer-events-none" style={{ bottom: composerHeight + 16 }}>
939
  <Button
@@ -948,40 +897,15 @@ export function ChatArea({
948
  </div>
949
  )}
950
 
951
- {/* =========================
952
- 3) Composer (bottom flex item; never scrolls)
953
- ========================= */}
954
  <div ref={composerRef} className="flex-shrink-0 bg-background/95 backdrop-blur-sm z-20 border-t border-border">
955
  <div className="max-w-4xl mx-auto px-4 py-4">
 
956
  {uploadedFiles.length > 0 && (
957
  <div className="mb-2 flex flex-wrap gap-2 max-h-32 overflow-y-auto">
958
- {uploadedFiles.map((uploadedFile, index) => {
959
- const Icon = getFileIcon(uploadedFile.file.name);
960
- const fileInfo = getFileTypeInfo(uploadedFile.file.name);
961
- const isImage = ["jpg", "jpeg", "png", "gif", "webp"].some((ext) =>
962
- uploadedFile.file.name.toLowerCase().endsWith(`.${ext}`)
963
- );
964
-
965
- return (
966
- <div key={index}>
967
- <FileThumbnail
968
- file={uploadedFile.file}
969
- Icon={Icon}
970
- fileInfo={fileInfo}
971
- isImage={isImage}
972
- onPreview={() => {
973
- setSelectedFile({ file: uploadedFile.file, index });
974
- setShowFileViewer(true);
975
- }}
976
- // ✅ CHANGE 2: 直接 set state,不传事件
977
- onRemove={() => {
978
- setFileToDelete(index);
979
- setShowDeleteDialog(true);
980
- }}
981
- />
982
- </div>
983
- );
984
- })}
985
  </div>
986
  )}
987
 
@@ -1012,30 +936,21 @@ export function ChatArea({
1012
  </Button>
1013
  </DropdownMenuTrigger>
1014
  <DropdownMenuContent align="start" className="w-56">
1015
- <DropdownMenuItem
1016
- onClick={() => onLearningModeChange("general")}
1017
- className={learningMode === "general" ? "bg-accent" : ""}
1018
- >
1019
  <div className="flex flex-col">
1020
  <span className="font-medium">General</span>
1021
  <span className="text-xs text-muted-foreground">Answer various questions (context required)</span>
1022
  </div>
1023
  </DropdownMenuItem>
1024
 
1025
- <DropdownMenuItem
1026
- onClick={() => onLearningModeChange("concept")}
1027
- className={learningMode === "concept" ? "bg-accent" : ""}
1028
- >
1029
  <div className="flex flex-col">
1030
  <span className="font-medium">Concept Explainer</span>
1031
  <span className="text-xs text-muted-foreground">Get detailed explanations of concepts</span>
1032
  </div>
1033
  </DropdownMenuItem>
1034
 
1035
- <DropdownMenuItem
1036
- onClick={() => onLearningModeChange("socratic")}
1037
- className={learningMode === "socratic" ? "bg-accent" : ""}
1038
- >
1039
  <div className="flex flex-col">
1040
  <span className="font-medium">Socratic Tutor</span>
1041
  <span className="text-xs text-muted-foreground">Learn through guided questions</span>
@@ -1049,20 +964,14 @@ export function ChatArea({
1049
  </div>
1050
  </DropdownMenuItem>
1051
 
1052
- <DropdownMenuItem
1053
- onClick={() => onLearningModeChange("assignment")}
1054
- className={learningMode === "assignment" ? "bg-accent" : ""}
1055
- >
1056
  <div className="flex flex-col">
1057
  <span className="font-medium">Assignment Helper</span>
1058
  <span className="text-xs text-muted-foreground">Get help with assignments</span>
1059
  </div>
1060
  </DropdownMenuItem>
1061
 
1062
- <DropdownMenuItem
1063
- onClick={() => onLearningModeChange("summary")}
1064
- className={learningMode === "summary" ? "bg-accent" : ""}
1065
- >
1066
  <div className="flex flex-col">
1067
  <span className="font-medium">Quick Summary</span>
1068
  <span className="text-xs text-muted-foreground">Get concise summaries</span>
@@ -1271,15 +1180,13 @@ export function ChatArea({
1271
  </DialogContent>
1272
  </Dialog>
1273
 
1274
- {/* Delete File Confirmation Dialog */}
1275
  <AlertDialog open={showDeleteDialog} onOpenChange={setShowDeleteDialog}>
1276
- {/* ✅ CHANGE 3: 提高 z-index,确保弹窗不会被遮挡 */}
1277
- <AlertDialogContent className="z-[99999]" style={{ zIndex: 99999 }}>
1278
  <AlertDialogHeader>
1279
  <AlertDialogTitle>Delete File</AlertDialogTitle>
1280
  <AlertDialogDescription>
1281
- Are you sure you want to delete &quot;
1282
- {fileToDelete !== null ? uploadedFiles[fileToDelete]?.file.name : ""}&quot;? This action cannot be undone.
1283
  </AlertDialogDescription>
1284
  </AlertDialogHeader>
1285
  <AlertDialogFooter>
 
50
  import { SmartReview } from "./SmartReview";
51
  import clareAvatar from "../assets/dfe44dab3ad8cd93953eac4a3e68bd1a5f999653.png";
52
 
53
+ // ✅ NEW
54
+ import { useObjectUrlCache } from "../lib/useObjectUrlCache";
55
+
56
  type ReviewEventType = "send_message" | "review_topic" | "review_all";
57
 
58
  interface ChatAreaProps {
 
92
  availableCourses?: Array<{ id: string; name: string }>;
93
  showReviewBanner?: boolean;
94
 
 
95
  onReviewActivity?: (event: ReviewEventType) => void;
96
  }
97
 
 
201
  return;
202
  }
203
 
 
204
  el.scrollTo({ top, behavior });
205
  };
206
 
 
207
  useEffect(() => {
208
  if (isInitialMount.current) {
209
  isInitialMount.current = false;
210
  previousMessagesLength.current = messages.length;
211
 
 
212
  const el = scrollContainerRef.current;
213
  if (el) el.scrollTop = 0;
214
  return;
 
224
  previousMessagesLength.current = messages.length;
225
  }, [messages]);
226
 
 
227
  useEffect(() => {
228
  const container = scrollContainerRef.current;
229
  if (!container) return;
 
244
  e.preventDefault();
245
  if (!input.trim() || !isLoggedIn) return;
246
 
 
247
  if (chatMode === "review") onReviewActivity?.("send_message");
248
 
249
  onSendMessage(input);
 
266
  summary: "Quick Summary",
267
  };
268
 
 
269
  const handleReviewTopic = (item: {
270
  title: string;
271
  previousQuestion: string;
 
275
  weight: number;
276
  lastReviewed: string;
277
  }) => {
 
278
  onReviewActivity?.("review_topic");
279
 
280
  const userMessage = `Please help me review: ${item.title}`;
 
284
  };
285
 
286
  const handleReviewAll = () => {
 
287
  onReviewActivity?.("review_all");
 
288
  (window as any).__lastReviewData = "REVIEW_ALL";
289
  onSendMessage("Please help me review all topics that need attention.");
290
  };
 
378
  }
379
  };
380
 
 
381
  const isCurrentChatSaved = (): boolean => {
382
  if (messages.length <= 1) return false;
383
 
 
433
  const saved = isCurrentChatSaved();
434
 
435
  if (saved) {
436
+ onConfirmClear(false as any);
437
  return;
438
  }
439
 
 
559
  return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
560
  };
561
 
562
+ // ✅ useObjectUrlCache: for image thumbnails (uploaded + pending)
563
+ const allThumbFiles = React.useMemo(() => {
564
+ return [
565
+ ...uploadedFiles.map((u) => u.file),
566
+ ...pendingFiles.map((p) => p.file),
567
+ ];
568
+ }, [uploadedFiles, pendingFiles]);
569
+ const { getOrCreate } = useObjectUrlCache(allThumbFiles);
570
+
571
+ // ✅ NEW: a compact "chip" UI (the one with left X)
572
+ const FileChip = ({
573
  file,
574
+ index,
 
 
 
 
575
  }: {
576
  file: File;
577
+ index: number;
 
 
 
 
578
  }) => {
579
+ const ext = file.name.toLowerCase();
580
+ const isImage = [".jpg", ".jpeg", ".png", ".gif", ".webp"].some((e) => ext.endsWith(e));
581
 
582
+ const label = ext.endsWith(".pdf")
583
+ ? "PDF"
584
+ : ext.endsWith(".pptx") || ext.endsWith(".ppt")
585
+ ? "Presentation"
586
+ : ext.endsWith(".docx") || ext.endsWith(".doc")
587
+ ? "Document"
588
+ : isImage
589
+ ? "Image"
590
+ : "File";
591
 
592
+ const thumbUrl = isImage ? getOrCreate(file) : null;
 
 
 
 
 
 
 
 
593
 
594
+ return (
595
+ <div className="flex items-center gap-3 rounded-xl border border-border bg-card px-3 py-2 shadow-sm w-[340px] max-w-full">
596
+ {/* ✅ FIX: type="button" + preventDefault/stopPropagation + direct remove */}
597
+ <button
598
+ type="button"
599
+ className="h-6 w-6 rounded-full border border-border bg-background flex items-center justify-center hover:bg-muted"
600
+ onClick={(e) => {
601
+ e.preventDefault();
602
+ e.stopPropagation();
603
+
604
+ // If user is previewing this file, close viewer safely
605
+ setSelectedFile((prev) => {
606
+ if (!prev) return prev;
607
+ if (prev.index === index) return null;
608
+ return prev;
609
+ });
610
+ setShowFileViewer(false);
611
+
612
+ onRemoveFile(index);
613
+ }}
614
+ aria-label="Remove file"
615
+ title="Remove"
616
  >
617
+ <X className="h-4 w-4" />
618
+ </button>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
619
 
620
+ <div className="min-w-0 flex-1">
621
+ <div className="text-sm font-medium truncate" title={file.name}>
622
+ {file.name}
 
 
 
 
 
 
 
 
623
  </div>
624
+ <div className="text-xs text-muted-foreground">{label}</div>
625
  </div>
 
 
626
 
627
+ {isImage ? (
628
+ <div className="relative h-14 w-14 flex-shrink-0 rounded-lg overflow-hidden border border-border">
629
+ {thumbUrl ? (
630
+ <img
631
+ src={thumbUrl}
632
+ alt={file.name}
633
+ className="h-full w-full object-cover"
634
+ draggable={false}
635
+ />
636
+ ) : (
637
+ <div className="h-full w-full flex items-center justify-center bg-muted">
638
+ <ImageIcon className="h-5 w-5 text-muted-foreground" />
639
+ </div>
640
+ )}
641
  </div>
642
+ ) : null}
 
 
 
 
 
 
 
 
 
 
 
 
643
  </div>
644
  );
645
  };
 
658
  const ext = file.name.toLowerCase();
659
 
660
  if ([".jpg", ".jpeg", ".png", ".gif", ".webp"].some((e) => ext.endsWith(e))) {
661
+ // Use objectURL for viewer as well
662
+ setContent(getOrCreate(file));
663
+ setLoading(false);
 
 
 
 
 
 
 
664
  return;
665
  }
666
 
 
687
  };
688
 
689
  loadFile();
690
+ // eslint-disable-next-line react-hooks/exhaustive-deps
691
  }, [file]);
692
 
693
  if (loading) return <div className="text-center py-8">Loading...</div>;
 
704
  );
705
  }
706
 
707
+ return (
708
+ <div className="whitespace-pre-wrap text-sm font-mono p-4 bg-muted rounded-lg max-h-[60vh] overflow-y-auto">
709
+ {content}
710
+ </div>
711
+ );
712
  };
713
 
 
714
  const bottomPad = Math.max(24, composerHeight + 24);
715
 
716
  return (
717
  <div className="relative flex flex-col h-full min-h-0 w-full overflow-hidden">
718
+ {/* Top Bar */}
 
 
719
  <div
720
  className={`flex-shrink-0 flex items-center justify-between px-4 bg-card z-20 ${
721
  showTopBorder ? "border-b border-border" : ""
 
728
  const current = workspaces.find((w) => w.id === currentWorkspaceId);
729
  if (current?.type === "group") {
730
  if (current.category === "course" && current.courseName) {
731
+ return <div className="h-9 px-3 inline-flex items-center rounded-md border font-semibold">{current.courseName}</div>;
 
 
 
 
732
  }
733
  return null;
734
  }
 
750
  })()}
751
  </div>
752
 
753
+ {/* Tabs - Center */}
754
  <div className="absolute left-1/2 -translate-x-1/2 flex-shrink-0">
755
  <Tabs value={chatMode} onValueChange={(value) => onChatModeChange(value as ChatMode)} className="w-auto" orientation="horizontal">
756
  <TabsList className="inline-flex h-8 items-center justify-center rounded-xl bg-muted p-1 text-muted-foreground">
 
825
  </div>
826
  </div>
827
 
828
+ {/* Scroll Container */}
 
 
829
  <div ref={scrollContainerRef} className="flex-1 min-h-0 overflow-y-auto overscroll-contain" style={{ overscrollBehavior: "contain" }}>
 
830
  <div className="py-6" style={{ paddingBottom: bottomPad }}>
831
  <div className="w-full space-y-6 max-w-4xl mx-auto">
832
  {messages.map((message) => (
 
834
  <Message
835
  message={message}
836
  showSenderInfo={spaceType === "group"}
837
+ isFirstGreeting={(message.id === "1" || message.id === "review-1" || message.id === "quiz-1") && message.role === "assistant"}
 
 
838
  showNextButton={message.showNextButton && !isAppTyping}
839
  onNextQuestion={onNextQuestion}
840
  chatMode={chatMode}
 
882
  </div>
883
  </div>
884
 
885
+ {/* Scroll-to-bottom button */}
 
 
886
  {showScrollButton && (
887
  <div className="absolute z-30 left-0 right-0 flex justify-center pointer-events-none" style={{ bottom: composerHeight + 16 }}>
888
  <Button
 
897
  </div>
898
  )}
899
 
900
+ {/* Composer */}
 
 
901
  <div ref={composerRef} className="flex-shrink-0 bg-background/95 backdrop-blur-sm z-20 border-t border-border">
902
  <div className="max-w-4xl mx-auto px-4 py-4">
903
+ {/* ✅ Uploaded Files Preview (chip UI) */}
904
  {uploadedFiles.length > 0 && (
905
  <div className="mb-2 flex flex-wrap gap-2 max-h-32 overflow-y-auto">
906
+ {uploadedFiles.map((u, index) => (
907
+ <FileChip key={`${u.file.name}-${u.file.size}-${u.file.lastModified}-${index}`} file={u.file} index={index} />
908
+ ))}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
909
  </div>
910
  )}
911
 
 
936
  </Button>
937
  </DropdownMenuTrigger>
938
  <DropdownMenuContent align="start" className="w-56">
939
+ <DropdownMenuItem onClick={() => onLearningModeChange("general")} className={learningMode === "general" ? "bg-accent" : ""}>
 
 
 
940
  <div className="flex flex-col">
941
  <span className="font-medium">General</span>
942
  <span className="text-xs text-muted-foreground">Answer various questions (context required)</span>
943
  </div>
944
  </DropdownMenuItem>
945
 
946
+ <DropdownMenuItem onClick={() => onLearningModeChange("concept")} className={learningMode === "concept" ? "bg-accent" : ""}>
 
 
 
947
  <div className="flex flex-col">
948
  <span className="font-medium">Concept Explainer</span>
949
  <span className="text-xs text-muted-foreground">Get detailed explanations of concepts</span>
950
  </div>
951
  </DropdownMenuItem>
952
 
953
+ <DropdownMenuItem onClick={() => onLearningModeChange("socratic")} className={learningMode === "socratic" ? "bg-accent" : ""}>
 
 
 
954
  <div className="flex flex-col">
955
  <span className="font-medium">Socratic Tutor</span>
956
  <span className="text-xs text-muted-foreground">Learn through guided questions</span>
 
964
  </div>
965
  </DropdownMenuItem>
966
 
967
+ <DropdownMenuItem onClick={() => onLearningModeChange("assignment")} className={learningMode === "assignment" ? "bg-accent" : ""}>
 
 
 
968
  <div className="flex flex-col">
969
  <span className="font-medium">Assignment Helper</span>
970
  <span className="text-xs text-muted-foreground">Get help with assignments</span>
971
  </div>
972
  </DropdownMenuItem>
973
 
974
+ <DropdownMenuItem onClick={() => onLearningModeChange("summary")} className={learningMode === "summary" ? "bg-accent" : ""}>
 
 
 
975
  <div className="flex flex-col">
976
  <span className="font-medium">Quick Summary</span>
977
  <span className="text-xs text-muted-foreground">Get concise summaries</span>
 
1180
  </DialogContent>
1181
  </Dialog>
1182
 
1183
+ {/* Delete File Confirmation Dialog (kept, but chip delete is instant now) */}
1184
  <AlertDialog open={showDeleteDialog} onOpenChange={setShowDeleteDialog}>
1185
+ <AlertDialogContent>
 
1186
  <AlertDialogHeader>
1187
  <AlertDialogTitle>Delete File</AlertDialogTitle>
1188
  <AlertDialogDescription>
1189
+ Are you sure you want to delete &quot;{fileToDelete !== null ? uploadedFiles[fileToDelete]?.file.name : ""}&quot;? This action cannot be undone.
 
1190
  </AlertDialogDescription>
1191
  </AlertDialogHeader>
1192
  <AlertDialogFooter>