SarahXia0405 commited on
Commit
825ec95
·
verified ·
1 Parent(s): 7073506

Update web/src/components/ChatArea.tsx

Browse files
Files changed (1) hide show
  1. web/src/components/ChatArea.tsx +416 -368
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; not used for positioning anymore
141
  currentCourseId,
142
  onCourseChange,
143
  availableCourses = [],
@@ -176,6 +176,8 @@ export function ChatArea({
176
  { id: "course4", name: "Web Development" },
177
  ];
178
 
 
 
179
  const messagesEndRef = useRef<HTMLDivElement>(null);
180
  const scrollContainerRef = useRef<HTMLDivElement>(null);
181
  const fileInputRef = useRef<HTMLInputElement>(null);
@@ -184,7 +186,8 @@ export function ChatArea({
184
  const previousMessagesLength = useRef(messages.length);
185
 
186
  const scrollToBottom = () => {
187
- messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
 
188
  };
189
 
190
  // Only auto-scroll when new messages are added (not on initial load)
@@ -193,6 +196,7 @@ export function ChatArea({
193
  isInitialMount.current = false;
194
  previousMessagesLength.current = messages.length;
195
 
 
196
  if (scrollContainerRef.current) {
197
  scrollContainerRef.current.scrollTop = 0;
198
  }
@@ -212,7 +216,7 @@ export function ChatArea({
212
  if (!el) return;
213
 
214
  const { scrollTop, scrollHeight, clientHeight } = el;
215
- const isAtBottom = scrollHeight - scrollTop - clientHeight < 100;
216
  setShowScrollButton(!isAtBottom);
217
  setShowTopBorder(scrollTop > 0);
218
  };
@@ -733,395 +737,424 @@ export function ChatArea({
733
  );
734
  };
735
 
 
 
 
 
 
 
 
736
  return (
737
- // IMPORTANT: relative enables internal absolute overlays (input + scroll button)
738
  <div className="relative flex flex-col h-full min-h-0 overflow-hidden">
739
- <div className="flex-1 relative min-h-0 flex flex-col overflow-hidden">
740
- {/* Messages Area is the ONLY scroll container */}
 
 
 
 
 
 
 
741
  <div
742
- ref={scrollContainerRef}
743
- className="flex-1 min-h-0 overflow-y-auto"
744
- style={{ overscrollBehavior: "contain" }}
 
745
  >
746
- {/* Top Bar - Sticky */}
747
- <div
748
- className={`sticky top-0 flex items-center justify-between px-4 z-20 bg-card ${
749
- showTopBorder ? "border-b border-border" : ""
750
- }`}
751
- style={{ height: "4.5rem", margin: 0, padding: "1rem 1rem", boxSizing: "border-box" }}
752
- >
753
- {/* Course Selector - Left */}
754
- <div className="flex-shrink-0">
755
- {(() => {
756
- const current = workspaces.find((w) => w.id === currentWorkspaceId);
757
- if (current?.type === "group") {
758
- if (current.category === "course" && current.courseName) {
759
- return (
760
- <div className="h-9 px-3 inline-flex items-center rounded-md border font-semibold">
761
- {current.courseName}
762
- </div>
763
- );
764
- }
765
- return null;
766
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
767
 
768
- return (
769
- <Select value={currentCourseId || "course1"} onValueChange={(val) => onCourseChange && onCourseChange(val)}>
770
- <SelectTrigger className="w-[200px] h-9 font-semibold">
771
- <SelectValue placeholder="Select course" />
772
- </SelectTrigger>
773
- <SelectContent>
774
- {courses.map((course) => (
775
- <SelectItem key={course.id} value={course.id}>
776
- {course.name}
777
- </SelectItem>
778
- ))}
779
- </SelectContent>
780
- </Select>
781
- );
782
- })()}
783
- </div>
784
-
785
- {/* Chat Mode Tabs - Center */}
786
- <div className="absolute left-1/2 -translate-x-1/2 flex-shrink-0">
787
- <Tabs
788
- value={chatMode}
789
- onValueChange={(value) => onChatModeChange(value as ChatMode)}
790
- className="w-auto"
791
- orientation="horizontal"
792
- >
793
- <TabsList className="inline-flex h-8 items-center justify-center rounded-xl bg-muted p-1 text-muted-foreground">
794
- <TabsTrigger value="ask" className="w-[140px] px-3 text-sm">
795
- Ask
796
- </TabsTrigger>
797
- <TabsTrigger value="review" className="w-[140px] px-3 text-sm relative">
798
- Review
799
- <span
800
- className="absolute top-0 right-0 bg-red-500 rounded-full border-2"
801
- style={{
802
- width: "10px",
803
- height: "10px",
804
- transform: "translate(25%, -25%)",
805
- zIndex: 10,
806
- borderColor: "var(--muted)",
807
- }}
808
- />
809
- </TabsTrigger>
810
- <TabsTrigger value="quiz" className="w-[140px] px-3 text-sm">
811
- Quiz
812
- </TabsTrigger>
813
- </TabsList>
814
- </Tabs>
815
- </div>
816
 
817
- {/* Action Buttons - Right */}
818
- <div className="flex items-center gap-2 flex-shrink-0">
819
- <Button
820
- variant="ghost"
821
- size="icon"
822
- onClick={handleSaveClick}
823
- disabled={!isLoggedIn}
824
- className={`h-8 w-8 rounded-md hover:bg-muted/50 ${isCurrentChatSaved() ? "text-primary" : ""}`}
825
- title={isCurrentChatSaved() ? "Unsave" : "Save"}
826
- >
827
- <Bookmark className={`h-4 w-4 ${isCurrentChatSaved() ? "fill-primary text-primary" : ""}`} />
828
- </Button>
829
 
830
- <Button
831
- variant="ghost"
832
- size="icon"
833
- onClick={handleOpenDownloadDialog}
834
- disabled={!isLoggedIn}
835
- className="h-8 w-8 rounded-md hover:bg-muted/50"
836
- title="Download"
837
- >
838
- <Download className="h-4 w-4" />
839
- </Button>
840
 
841
- <Button
842
- variant="ghost"
843
- size="icon"
844
- onClick={handleShareClick}
845
- disabled={!isLoggedIn}
846
- className="h-8 w-8 rounded-md hover:bg-muted/50"
847
- title="Share"
848
- >
849
- <Share2 className="h-4 w-4" />
850
- </Button>
851
 
852
- <Button
853
- variant="outline"
854
- onClick={handleClearClick}
855
- disabled={!isLoggedIn}
856
- 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)]"
857
- title="New Chat"
858
- >
859
- <Plus className="h-4 w-4" />
860
- <span className="text-sm font-medium">New chat</span>
861
- </Button>
862
- </div>
863
  </div>
 
864
 
865
- {/* Messages Content */}
866
- <div className="py-6" style={{ paddingBottom: "12rem" }}>
867
- <div className="w-full space-y-6 max-w-4xl mx-auto">
868
- {messages.map((message) => (
869
- <React.Fragment key={message.id}>
870
- <Message
871
- message={message}
872
- showSenderInfo={spaceType === "group"}
873
- isFirstGreeting={
874
- (message.id === "1" || message.id === "review-1" || message.id === "quiz-1") &&
875
- message.role === "assistant"
876
- }
877
- showNextButton={message.showNextButton && !isAppTyping}
878
- onNextQuestion={onNextQuestion}
879
- chatMode={chatMode}
880
- />
881
 
882
- {chatMode === "review" && message.id === "review-1" && message.role === "assistant" && (
883
- <div className="flex gap-2 justify-start px-4">
884
- <div className="w-10 h-10 flex-shrink-0" />
885
- <div className="w-full" style={{ maxWidth: "min(770px, calc(100% - 2rem))" }}>
886
- <SmartReview onReviewTopic={handleReviewTopic} onReviewAll={handleReviewAll} />
887
- </div>
888
  </div>
889
- )}
890
-
891
- {chatMode === "quiz" &&
892
- message.id === "quiz-1" &&
893
- message.role === "assistant" &&
894
- quizState.currentQuestion === 0 &&
895
- !quizState.waitingForAnswer &&
896
- !isAppTyping && (
897
- <div className="flex justify-center py-4">
898
- <Button onClick={onStartQuiz} className="bg-red-500 hover:bg-red-600 text-white">
899
- Start Quiz
900
- </Button>
901
- </div>
902
- )}
903
- </React.Fragment>
904
- ))}
905
-
906
- {isAppTyping && (
907
- <div className="flex gap-2 justify-start px-4">
908
- <div className="w-10 h-10 rounded-full overflow-hidden bg-white flex items-center justify-center flex-shrink-0">
909
- <img src={clareAvatar} alt="Clare" className="w-full h-full object-cover" />
910
  </div>
911
- <div className="bg-muted rounded-2xl px-4 py-3">
912
- <div className="flex gap-1">
913
- <div className="w-2 h-2 rounded-full bg-muted-foreground/50 animate-bounce" style={{ animationDelay: "0ms" }} />
914
- <div className="w-2 h-2 rounded-full bg-muted-foreground/50 animate-bounce" style={{ animationDelay: "150ms" }} />
915
- <div className="w-2 h-2 rounded-full bg-muted-foreground/50 animate-bounce" style={{ animationDelay: "300ms" }} />
 
 
 
 
 
 
 
916
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
917
  </div>
918
  </div>
919
- )}
 
920
 
921
- <div ref={messagesEndRef} />
922
- </div>
923
  </div>
924
  </div>
 
925
 
926
- {/* Scroll to Bottom Button (NOW absolute inside ChatArea) */}
927
- {showScrollButton && (
928
- <div
929
- className="absolute z-30 left-0 right-0 flex justify-center pointer-events-none"
930
- style={{ bottom: "120px" }}
 
 
 
 
 
 
931
  >
932
- <Button
933
- variant="secondary"
934
- size="icon"
935
- className="rounded-full shadow-lg hover:shadow-xl transition-shadow bg-background border border-border pointer-events-auto w-10 h-10"
936
- onClick={scrollToBottom}
937
- title="Scroll to bottom"
938
- >
939
- <ArrowDown className="h-5 w-5" />
940
- </Button>
941
- </div>
942
- )}
943
-
944
- {/* Floating Input Area (NOW absolute inside ChatArea) */}
945
- <div className="absolute bottom-0 left-0 right-0 bg-background/95 backdrop-blur-sm z-10">
946
- <div className="max-w-4xl mx-auto px-4 py-4">
947
- {uploadedFiles.length > 0 && (
948
- <div className="mb-2 flex flex-wrap gap-2 max-h-32 overflow-y-auto">
949
- {uploadedFiles.map((uploadedFile, index) => {
950
- const Icon = getFileIcon(uploadedFile.file.name);
951
- const fileInfo = getFileTypeInfo(uploadedFile.file.name);
952
- const isImage = ["jpg", "jpeg", "png", "gif", "webp"].some((ext) =>
953
- uploadedFile.file.name.toLowerCase().endsWith(`.${ext}`)
954
- );
955
-
956
- return (
957
- <div key={index}>
958
- <FileThumbnail
959
- file={uploadedFile.file}
960
- Icon={Icon}
961
- fileInfo={fileInfo}
962
- isImage={isImage}
963
- onPreview={() => {
964
- setSelectedFile({ file: uploadedFile.file, index });
965
- setShowFileViewer(true);
966
- }}
967
- onRemove={(e) => {
968
- e.stopPropagation();
969
- setFileToDelete(index);
970
- setShowDeleteDialog(true);
971
- }}
972
- />
973
- </div>
974
- );
975
- })}
976
- </div>
977
- )}
978
-
979
- <form
980
- onSubmit={handleSubmit}
981
- onDragOver={handleDragOver}
982
- onDragLeave={handleDragLeave}
983
- onDrop={handleDrop}
984
- className={isDragging ? "opacity-75" : ""}
985
- >
986
- <div className="relative">
987
- {/* Mode Selector + Upload */}
988
- <div className="absolute bottom-3 left-2 flex items-center gap-1 z-10">
989
- {chatMode === "ask" && (
990
- <DropdownMenu>
991
- <DropdownMenuTrigger asChild>
992
- <Button
993
- variant="ghost"
994
- size="sm"
995
- className="gap-1.5 h-8 px-2 text-xs hover:bg-muted/50"
996
- disabled={!isLoggedIn}
997
- type="button"
998
- >
999
- <span>{modeLabels[learningMode]}</span>
1000
- <svg className="h-3 w-3 opacity-50" fill="none" stroke="currentColor" viewBox="0 0 24 24">
1001
- <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
1002
- </svg>
1003
- </Button>
1004
- </DropdownMenuTrigger>
1005
- <DropdownMenuContent align="start" className="w-56">
1006
- <DropdownMenuItem
1007
- onClick={() => onLearningModeChange("general")}
1008
- className={learningMode === "general" ? "bg-accent" : ""}
1009
- >
1010
- <div className="flex flex-col">
1011
- <span className="font-medium">General</span>
1012
- <span className="text-xs text-muted-foreground">Answer various questions (context required)</span>
1013
- </div>
1014
- </DropdownMenuItem>
1015
-
1016
- <DropdownMenuItem
1017
- onClick={() => onLearningModeChange("concept")}
1018
- className={learningMode === "concept" ? "bg-accent" : ""}
1019
- >
1020
- <div className="flex flex-col">
1021
- <span className="font-medium">Concept Explainer</span>
1022
- <span className="text-xs text-muted-foreground">Get detailed explanations of concepts</span>
1023
- </div>
1024
- </DropdownMenuItem>
1025
-
1026
- <DropdownMenuItem
1027
- onClick={() => onLearningModeChange("socratic")}
1028
- className={learningMode === "socratic" ? "bg-accent" : ""}
1029
- >
1030
- <div className="flex flex-col">
1031
- <span className="font-medium">Socratic Tutor</span>
1032
- <span className="text-xs text-muted-foreground">Learn through guided questions</span>
1033
- </div>
1034
- </DropdownMenuItem>
1035
-
1036
- <DropdownMenuItem
1037
- onClick={() => onLearningModeChange("exam")}
1038
- className={learningMode === "exam" ? "bg-accent" : ""}
1039
- >
1040
- <div className="flex flex-col">
1041
- <span className="font-medium">Exam Prep</span>
1042
- <span className="text-xs text-muted-foreground">Practice with quiz questions</span>
1043
- </div>
1044
- </DropdownMenuItem>
1045
-
1046
- <DropdownMenuItem
1047
- onClick={() => onLearningModeChange("assignment")}
1048
- className={learningMode === "assignment" ? "bg-accent" : ""}
1049
- >
1050
- <div className="flex flex-col">
1051
- <span className="font-medium">Assignment Helper</span>
1052
- <span className="text-xs text-muted-foreground">Get help with assignments</span>
1053
- </div>
1054
- </DropdownMenuItem>
1055
-
1056
- <DropdownMenuItem
1057
- onClick={() => onLearningModeChange("summary")}
1058
- className={learningMode === "summary" ? "bg-accent" : ""}
1059
- >
1060
- <div className="flex flex-col">
1061
- <span className="font-medium">Quick Summary</span>
1062
- <span className="text-xs text-muted-foreground">Get concise summaries</span>
1063
- </div>
1064
- </DropdownMenuItem>
1065
- </DropdownMenuContent>
1066
- </DropdownMenu>
1067
- )}
1068
 
1069
- <Button
1070
- type="button"
1071
- size="icon"
1072
- variant="ghost"
1073
- disabled={!isLoggedIn || (chatMode === "quiz" && !quizState.waitingForAnswer)}
1074
- className="h-8 w-8 hover:bg-muted/50"
1075
- onClick={() => fileInputRef.current?.click()}
1076
- title="Upload files"
1077
- >
1078
- <Upload className="h-4 w-4" />
1079
- </Button>
1080
- </div>
 
 
1081
 
1082
- <Textarea
1083
- value={input}
1084
- onChange={(e) => setInput(e.target.value)}
1085
- onKeyDown={handleKeyDown}
1086
- placeholder={
1087
- !isLoggedIn
1088
- ? "Please log in on the right to start chatting..."
1089
- : chatMode === "quiz"
1090
- ? quizState.waitingForAnswer
1091
- ? "Type your answer here..."
1092
- : quizState.currentQuestion > 0
1093
- ? "Click 'Next Question' to continue..."
1094
- : "Click 'Start Quiz' to begin..."
1095
- : spaceType === "group"
1096
- ? "Type a message or drag files here... (mention @Clare to get AI assistance)"
1097
- : learningMode === "general"
1098
- ? "Ask me anything! Please provide context about your question..."
1099
- : "Ask Clare anything about the course or drag files here..."
1100
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1101
  disabled={!isLoggedIn || (chatMode === "quiz" && !quizState.waitingForAnswer)}
1102
- className={`min-h-[80px] pl-4 pr-20 resize-none bg-background border-2 ${
1103
- isDragging ? "border-primary border-dashed" : "border-border"
1104
- }`}
1105
- />
 
 
 
1106
 
1107
- <div className="absolute bottom-2 right-2 flex gap-1">
1108
- <Button type="submit" size="icon" disabled={!input.trim() || !isLoggedIn} className="h-8 w-8 rounded-full">
1109
- <Send className="h-4 w-4" />
1110
- </Button>
1111
- </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1112
 
1113
- <input
1114
- ref={fileInputRef}
1115
- type="file"
1116
- multiple
1117
- accept=".pdf,.docx,.pptx,.doc,.ppt,.jpg,.jpeg,.png,.gif,.webp"
1118
- onChange={handleFileSelect}
1119
- className="hidden"
1120
- disabled={!isLoggedIn}
1121
- />
1122
  </div>
1123
- </form>
1124
- </div>
 
 
 
 
 
 
 
 
 
 
1125
  </div>
1126
  </div>
1127
 
@@ -1184,7 +1217,13 @@ export function ChatArea({
1184
  <div className="border rounded-lg bg-muted/40 flex flex-col max-h-64">
1185
  <div className="flex items-center justify-between p-4 sticky top-0 bg-muted/40 border-b z-10">
1186
  <span className="text-sm font-medium">Preview</span>
1187
- <Button variant="outline" size="sm" className="h-7 px-2 text-xs gap-1.5" onClick={handleCopyPreview} title="Copy preview">
 
 
 
 
 
 
1188
  <Copy className="h-3 w-3" />
1189
  Copy
1190
  </Button>
@@ -1259,7 +1298,9 @@ export function ChatArea({
1259
  ))}
1260
  </SelectContent>
1261
  </Select>
1262
- <p className="text-xs text-muted-foreground">Sends this conversation to the selected workspace&apos;s Saved Files.</p>
 
 
1263
  <Button onClick={handleShareSendToWorkspace} className="w-full">
1264
  Send
1265
  </Button>
@@ -1274,7 +1315,9 @@ export function ChatArea({
1274
  <AlertDialogHeader>
1275
  <AlertDialogTitle>Delete File</AlertDialogTitle>
1276
  <AlertDialogDescription>
1277
- Are you sure you want to delete &quot;{fileToDelete !== null ? uploadedFiles[fileToDelete]?.file.name : ""}&quot;? This action cannot be undone.
 
 
1278
  </AlertDialogDescription>
1279
  </AlertDialogHeader>
1280
  <AlertDialogFooter>
@@ -1306,7 +1349,9 @@ export function ChatArea({
1306
  </DialogTitle>
1307
  <DialogDescription>File size: {selectedFile ? formatFileSize(selectedFile.file.size) : ""}</DialogDescription>
1308
  </DialogHeader>
1309
- <div className="flex-1 min-h-0 overflow-y-auto mt-4">{selectedFile && <FileViewerContent file={selectedFile.file} />}</div>
 
 
1310
  </DialogContent>
1311
  </Dialog>
1312
 
@@ -1334,7 +1379,10 @@ export function ChatArea({
1334
 
1335
  <div className="space-y-1">
1336
  <label className="text-xs text-muted-foreground">File Type</label>
1337
- <Select value={pendingFile.type} onValueChange={(value) => handlePendingFileTypeChange(index, value as FileType)}>
 
 
 
1338
  <SelectTrigger className="h-8 text-xs">
1339
  <SelectValue />
1340
  </SelectTrigger>
 
137
  workspaces,
138
  currentWorkspaceId,
139
  onSaveFile,
140
+ leftPanelVisible = false, // kept for props compatibility
141
  currentCourseId,
142
  onCourseChange,
143
  availableCourses = [],
 
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
  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)
 
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;
202
  }
 
216
  if (!el) return;
217
 
218
  const { scrollTop, scrollHeight, clientHeight } = el;
219
+ const isAtBottom = scrollHeight - scrollTop - clientHeight < 120;
220
  setShowScrollButton(!isAtBottom);
221
  setShowTopBorder(scrollTop > 0);
222
  };
 
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) => (
884
+ <React.Fragment key={message.id}>
885
+ <Message
886
+ message={message}
887
+ showSenderInfo={spaceType === "group"}
888
+ isFirstGreeting={
889
+ (message.id === "1" || message.id === "review-1" || message.id === "quiz-1") &&
890
+ message.role === "assistant"
891
+ }
892
+ showNextButton={message.showNextButton && !isAppTyping}
893
+ onNextQuestion={onNextQuestion}
894
+ chatMode={chatMode}
895
+ />
896
 
897
+ {chatMode === "review" && message.id === "review-1" && message.role === "assistant" && (
898
+ <div className="flex gap-2 justify-start px-4">
899
+ <div className="w-10 h-10 flex-shrink-0" />
900
+ <div className="w-full" style={{ maxWidth: "min(770px, calc(100% - 2rem))" }}>
901
+ <SmartReview onReviewTopic={handleReviewTopic} onReviewAll={handleReviewAll} />
 
902
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
903
  </div>
904
+ )}
905
+
906
+ {chatMode === "quiz" &&
907
+ message.id === "quiz-1" &&
908
+ message.role === "assistant" &&
909
+ quizState.currentQuestion === 0 &&
910
+ !quizState.waitingForAnswer &&
911
+ !isAppTyping && (
912
+ <div className="flex justify-center py-4">
913
+ <Button onClick={onStartQuiz} className="bg-red-500 hover:bg-red-600 text-white">
914
+ Start Quiz
915
+ </Button>
916
  </div>
917
+ )}
918
+ </React.Fragment>
919
+ ))}
920
+
921
+ {isAppTyping && (
922
+ <div className="flex gap-2 justify-start px-4">
923
+ <div className="w-10 h-10 rounded-full overflow-hidden bg-white flex items-center justify-center flex-shrink-0">
924
+ <img src={clareAvatar} alt="Clare" className="w-full h-full object-cover" />
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" }}>
956
+ <Button
957
+ variant="secondary"
958
+ size="icon"
959
+ className="rounded-full shadow-lg hover:shadow-xl transition-shadow bg-background border border-border pointer-events-auto w-10 h-10"
960
+ onClick={scrollToBottom}
961
+ title="Scroll to bottom"
962
  >
963
+ <ArrowDown className="h-5 w-5" />
964
+ </Button>
965
+ </div>
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">
976
+ {uploadedFiles.map((uploadedFile, index) => {
977
+ const Icon = getFileIcon(uploadedFile.file.name);
978
+ const fileInfo = getFileTypeInfo(uploadedFile.file.name);
979
+ const isImage = ["jpg", "jpeg", "png", "gif", "webp"].some((ext) =>
980
+ uploadedFile.file.name.toLowerCase().endsWith(`.${ext}`)
981
+ );
982
 
983
+ return (
984
+ <div key={index}>
985
+ <FileThumbnail
986
+ file={uploadedFile.file}
987
+ Icon={Icon}
988
+ fileInfo={fileInfo}
989
+ isImage={isImage}
990
+ onPreview={() => {
991
+ setSelectedFile({ file: uploadedFile.file, index });
992
+ setShowFileViewer(true);
993
+ }}
994
+ onRemove={(e) => {
995
+ e.stopPropagation();
996
+ setFileToDelete(index);
997
+ setShowDeleteDialog(true);
998
+ }}
999
+ />
1000
+ </div>
1001
+ );
1002
+ })}
1003
+ </div>
1004
+ )}
1005
+
1006
+ <form
1007
+ onSubmit={handleSubmit}
1008
+ onDragOver={handleDragOver}
1009
+ onDragLeave={handleDragLeave}
1010
+ onDrop={handleDrop}
1011
+ className={isDragging ? "opacity-75" : ""}
1012
+ >
1013
+ <div className="relative">
1014
+ {/* Mode Selector + Upload */}
1015
+ <div className="absolute bottom-3 left-2 flex items-center gap-1 z-10">
1016
+ {chatMode === "ask" && (
1017
+ <DropdownMenu>
1018
+ <DropdownMenuTrigger asChild>
1019
+ <Button
1020
+ variant="ghost"
1021
+ size="sm"
1022
+ className="gap-1.5 h-8 px-2 text-xs hover:bg-muted/50"
1023
+ disabled={!isLoggedIn}
1024
+ type="button"
1025
+ >
1026
+ <span>{modeLabels[learningMode]}</span>
1027
+ <svg className="h-3 w-3 opacity-50" fill="none" stroke="currentColor" viewBox="0 0 24 24">
1028
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
1029
+ </svg>
1030
+ </Button>
1031
+ </DropdownMenuTrigger>
1032
+ <DropdownMenuContent align="start" className="w-56">
1033
+ <DropdownMenuItem
1034
+ onClick={() => onLearningModeChange("general")}
1035
+ className={learningMode === "general" ? "bg-accent" : ""}
1036
+ >
1037
+ <div className="flex flex-col">
1038
+ <span className="font-medium">General</span>
1039
+ <span className="text-xs text-muted-foreground">
1040
+ Answer various questions (context required)
1041
+ </span>
1042
+ </div>
1043
+ </DropdownMenuItem>
1044
+
1045
+ <DropdownMenuItem
1046
+ onClick={() => onLearningModeChange("concept")}
1047
+ className={learningMode === "concept" ? "bg-accent" : ""}
1048
+ >
1049
+ <div className="flex flex-col">
1050
+ <span className="font-medium">Concept Explainer</span>
1051
+ <span className="text-xs text-muted-foreground">Get detailed explanations of concepts</span>
1052
+ </div>
1053
+ </DropdownMenuItem>
1054
+
1055
+ <DropdownMenuItem
1056
+ onClick={() => onLearningModeChange("socratic")}
1057
+ className={learningMode === "socratic" ? "bg-accent" : ""}
1058
+ >
1059
+ <div className="flex flex-col">
1060
+ <span className="font-medium">Socratic Tutor</span>
1061
+ <span className="text-xs text-muted-foreground">Learn through guided questions</span>
1062
+ </div>
1063
+ </DropdownMenuItem>
1064
+
1065
+ <DropdownMenuItem
1066
+ onClick={() => onLearningModeChange("exam")}
1067
+ className={learningMode === "exam" ? "bg-accent" : ""}
1068
+ >
1069
+ <div className="flex flex-col">
1070
+ <span className="font-medium">Exam Prep</span>
1071
+ <span className="text-xs text-muted-foreground">Practice with quiz questions</span>
1072
+ </div>
1073
+ </DropdownMenuItem>
1074
+
1075
+ <DropdownMenuItem
1076
+ onClick={() => onLearningModeChange("assignment")}
1077
+ className={learningMode === "assignment" ? "bg-accent" : ""}
1078
+ >
1079
+ <div className="flex flex-col">
1080
+ <span className="font-medium">Assignment Helper</span>
1081
+ <span className="text-xs text-muted-foreground">Get help with assignments</span>
1082
+ </div>
1083
+ </DropdownMenuItem>
1084
+
1085
+ <DropdownMenuItem
1086
+ onClick={() => onLearningModeChange("summary")}
1087
+ className={learningMode === "summary" ? "bg-accent" : ""}
1088
+ >
1089
+ <div className="flex flex-col">
1090
+ <span className="font-medium">Quick Summary</span>
1091
+ <span className="text-xs text-muted-foreground">Get concise summaries</span>
1092
+ </div>
1093
+ </DropdownMenuItem>
1094
+ </DropdownMenuContent>
1095
+ </DropdownMenu>
1096
+ )}
1097
+
1098
+ <Button
1099
+ type="button"
1100
+ size="icon"
1101
+ variant="ghost"
1102
  disabled={!isLoggedIn || (chatMode === "quiz" && !quizState.waitingForAnswer)}
1103
+ className="h-8 w-8 hover:bg-muted/50"
1104
+ onClick={() => fileInputRef.current?.click()}
1105
+ title="Upload files"
1106
+ >
1107
+ <Upload className="h-4 w-4" />
1108
+ </Button>
1109
+ </div>
1110
 
1111
+ <Textarea
1112
+ value={input}
1113
+ onChange={(e) => setInput(e.target.value)}
1114
+ onKeyDown={handleKeyDown}
1115
+ placeholder={
1116
+ !isLoggedIn
1117
+ ? "Please log in on the right to start chatting..."
1118
+ : chatMode === "quiz"
1119
+ ? quizState.waitingForAnswer
1120
+ ? "Type your answer here..."
1121
+ : quizState.currentQuestion > 0
1122
+ ? "Click 'Next Question' to continue..."
1123
+ : "Click 'Start Quiz' to begin..."
1124
+ : spaceType === "group"
1125
+ ? "Type a message or drag files here... (mention @Clare to get AI assistance)"
1126
+ : learningMode === "general"
1127
+ ? "Ask me anything! Please provide context about your question..."
1128
+ : "Ask Clare anything about the course or drag files here..."
1129
+ }
1130
+ disabled={!isLoggedIn || (chatMode === "quiz" && !quizState.waitingForAnswer)}
1131
+ className={`min-h-[80px] pl-4 pr-20 resize-none bg-background border-2 ${
1132
+ isDragging ? "border-primary border-dashed" : "border-border"
1133
+ }`}
1134
+ />
1135
 
1136
+ <div className="absolute bottom-2 right-2 flex gap-1">
1137
+ <Button
1138
+ type="submit"
1139
+ size="icon"
1140
+ disabled={!input.trim() || !isLoggedIn}
1141
+ className="h-8 w-8 rounded-full"
1142
+ >
1143
+ <Send className="h-4 w-4" />
1144
+ </Button>
1145
  </div>
1146
+
1147
+ <input
1148
+ ref={fileInputRef}
1149
+ type="file"
1150
+ multiple
1151
+ accept=".pdf,.docx,.pptx,.doc,.ppt,.jpg,.jpeg,.png,.gif,.webp"
1152
+ onChange={handleFileSelect}
1153
+ className="hidden"
1154
+ disabled={!isLoggedIn}
1155
+ />
1156
+ </div>
1157
+ </form>
1158
  </div>
1159
  </div>
1160
 
 
1217
  <div className="border rounded-lg bg-muted/40 flex flex-col max-h-64">
1218
  <div className="flex items-center justify-between p-4 sticky top-0 bg-muted/40 border-b z-10">
1219
  <span className="text-sm font-medium">Preview</span>
1220
+ <Button
1221
+ variant="outline"
1222
+ size="sm"
1223
+ className="h-7 px-2 text-xs gap-1.5"
1224
+ onClick={handleCopyPreview}
1225
+ title="Copy preview"
1226
+ >
1227
  <Copy className="h-3 w-3" />
1228
  Copy
1229
  </Button>
 
1298
  ))}
1299
  </SelectContent>
1300
  </Select>
1301
+ <p className="text-xs text-muted-foreground">
1302
+ Sends this conversation to the selected workspace&apos;s Saved Files.
1303
+ </p>
1304
  <Button onClick={handleShareSendToWorkspace} className="w-full">
1305
  Send
1306
  </Button>
 
1315
  <AlertDialogHeader>
1316
  <AlertDialogTitle>Delete File</AlertDialogTitle>
1317
  <AlertDialogDescription>
1318
+ Are you sure you want to delete &quot;
1319
+ {fileToDelete !== null ? uploadedFiles[fileToDelete]?.file.name : ""}
1320
+ &quot;? This action cannot be undone.
1321
  </AlertDialogDescription>
1322
  </AlertDialogHeader>
1323
  <AlertDialogFooter>
 
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
 
 
1379
 
1380
  <div className="space-y-1">
1381
  <label className="text-xs text-muted-foreground">File Type</label>
1382
+ <Select
1383
+ value={pendingFile.type}
1384
+ onValueChange={(value) => handlePendingFileTypeChange(index, value as FileType)}
1385
+ >
1386
  <SelectTrigger className="h-8 text-xs">
1387
  <SelectValue />
1388
  </SelectTrigger>