SarahXia0405 commited on
Commit
08e6135
·
verified ·
1 Parent(s): 9bd5080

Update web/src/components/ChatArea.tsx

Browse files
Files changed (1) hide show
  1. web/src/components/ChatArea.tsx +56 -102
web/src/components/ChatArea.tsx CHANGED
@@ -125,7 +125,7 @@ interface ChatAreaProps {
125
 
126
  onReviewActivity?: (event: ReviewEventType) => void;
127
  currentUserId?: string; // backend user_id
128
- docType?: string; // backend doc_type (optional)
129
  }
130
 
131
  interface PendingFile {
@@ -133,16 +133,6 @@ interface PendingFile {
133
  type: FileType;
134
  }
135
 
136
- // 2) PDF
137
- if (ext === "pdf") return pdfIcon;
138
-
139
- // 3) PPT / PPTX
140
- if (ext === "ppt" || ext === "pptx") return pptIcon;
141
-
142
- // 4) 其它
143
- return otherIcon;
144
- }
145
-
146
  export function ChatArea({
147
  messages,
148
  onSendMessage,
@@ -268,8 +258,7 @@ export function ChatArea({
268
  if (messages.length > previousMessagesLength.current) {
269
  const el = scrollContainerRef.current;
270
  if (el) {
271
- const nearBottom =
272
- el.scrollHeight - el.scrollTop - el.clientHeight < 240;
273
  if (nearBottom) scrollToBottom("smooth");
274
  }
275
  }
@@ -344,10 +333,7 @@ export function ChatArea({
344
  const buildPreviewContent = () => {
345
  if (!messages.length) return "";
346
  return messages
347
- .map(
348
- (msg) =>
349
- `${msg.role === "user" ? "You" : "Clare"}: ${msg.content}`
350
- )
351
  .join("\n\n");
352
  };
353
 
@@ -365,9 +351,7 @@ export function ChatArea({
365
  summary += `Key Points:\n`;
366
  userMessages.slice(0, 3).forEach((msg, idx) => {
367
  const preview = msg.content.substring(0, 80);
368
- summary += `${idx + 1}. ${preview}${
369
- msg.content.length > 80 ? "..." : ""
370
- }\n`;
371
  });
372
 
373
  return summary;
@@ -500,6 +484,7 @@ export function ChatArea({
500
  const saved = isCurrentChatSaved();
501
 
502
  if (saved) {
 
503
  onConfirmClear(false as any);
504
  return;
505
  }
@@ -557,9 +542,7 @@ export function ChatArea({
557
  });
558
 
559
  if (validFiles.length > 0) {
560
- setPendingFiles(
561
- validFiles.map((file) => ({ file, type: "other" as FileType }))
562
- );
563
  setShowTypeDialog(true);
564
  } else {
565
  toast.error("Please upload .pdf, .docx, .pptx, or image files");
@@ -586,9 +569,7 @@ export function ChatArea({
586
  });
587
 
588
  if (validFiles.length > 0) {
589
- setPendingFiles(
590
- validFiles.map((file) => ({ file, type: "other" as FileType }))
591
- );
592
  setShowTypeDialog(true);
593
  } else {
594
  toast.error("Please upload .pdf, .docx, .pptx, or image files");
@@ -619,9 +600,7 @@ export function ChatArea({
619
  };
620
 
621
  const handlePendingFileTypeChange = (index: number, type: FileType) => {
622
- setPendingFiles((prev) =>
623
- prev.map((pf, i) => (i === index ? { ...pf, type } : pf))
624
- );
625
  };
626
 
627
  // File helpers
@@ -630,8 +609,7 @@ export function ChatArea({
630
  if (ext.endsWith(".pdf")) return FileText;
631
  if (ext.endsWith(".docx") || ext.endsWith(".doc")) return File;
632
  if (ext.endsWith(".pptx") || ext.endsWith(".ppt")) return Presentation;
633
- if ([".jpg", ".jpeg", ".png", ".gif", ".webp"].some((e) => ext.endsWith(e)))
634
- return ImageIcon;
635
  return File;
636
  };
637
 
@@ -643,13 +621,14 @@ export function ChatArea({
643
 
644
  const fileKey = (f: File) => `${f.name}::${f.size}::${f.lastModified}`;
645
 
646
- // useObjectUrlCache: for image thumbnails (uploaded + pending)
647
  const allThumbFiles = useMemo(() => {
648
  return [...uploadedFiles.map((u) => u.file), ...pendingFiles.map((p) => p.file)];
649
  }, [uploadedFiles, pendingFiles]);
650
 
 
651
 
652
- // NEW: a compact "chip" UI (the one with left X)
653
  const FileChip = ({
654
  file,
655
  index,
@@ -662,9 +641,13 @@ export function ChatArea({
662
  const ext = file.name.toLowerCase();
663
  const isImage = [".jpg", ".jpeg", ".png", ".gif", ".webp"].some((e) => ext.endsWith(e));
664
 
665
- const label = ext.endsWith(".pdf")
 
 
 
 
666
  ? "PDF"
667
- : ext.endsWith(".pptx") || ext.endsWith(".ppt")
668
  ? "Presentation"
669
  : ext.endsWith(".docx") || ext.endsWith(".doc")
670
  ? "Document"
@@ -701,9 +684,10 @@ export function ChatArea({
701
  <div className="text-xs text-muted-foreground">{label}</div>
702
  </div>
703
 
704
- {isImage ? (
705
- <div className="relative h-10 w-10 flex-shrink-0 rounded-lg overflow-hidden border border-border bg-muted">
706
- {thumbUrl ? (
 
707
  <img
708
  src={thumbUrl}
709
  alt={file.name}
@@ -714,9 +698,16 @@ export function ChatArea({
714
  <div className="h-full w-full flex items-center justify-center">
715
  <ImageIcon className="h-4 w-4 text-muted-foreground" />
716
  </div>
717
- )}
718
- </div>
719
- ) : null}
 
 
 
 
 
 
 
720
  </div>
721
  );
722
  };
@@ -811,16 +802,10 @@ export function ChatArea({
811
  size="icon"
812
  onClick={handleSaveClick}
813
  disabled={!isLoggedIn}
814
- className={`h-8 w-8 rounded-md hover:bg-muted/50 ${
815
- isCurrentChatSaved() ? "text-primary" : ""
816
- }`}
817
  title={isCurrentChatSaved() ? "Unsave" : "Save"}
818
  >
819
- <Bookmark
820
- className={`h-4 w-4 ${
821
- isCurrentChatSaved() ? "fill-primary text-primary" : ""
822
- }`}
823
- />
824
  </Button>
825
 
826
  <Button
@@ -872,9 +857,7 @@ export function ChatArea({
872
  message={message}
873
  showSenderInfo={spaceType === "group"}
874
  isFirstGreeting={
875
- (message.id === "1" ||
876
- message.id === "review-1" ||
877
- message.id === "quiz-1") &&
878
  message.role === "assistant"
879
  }
880
  showNextButton={message.showNextButton && !isAppTyping}
@@ -885,24 +868,14 @@ export function ChatArea({
885
  docType={docType}
886
  />
887
 
888
- {chatMode === "review" &&
889
- message.id === "review-1" &&
890
- message.role === "assistant" && (
891
- <div className="flex gap-2 justify-start px-4">
892
- <div className="w-10 h-10 flex-shrink-0" />
893
- <div
894
- className="w-full"
895
- style={{
896
- maxWidth: "min(770px, calc(100% - 2rem))",
897
- }}
898
- >
899
- <SmartReview
900
- onReviewTopic={handleReviewTopic}
901
- onReviewAll={handleReviewAll}
902
- />
903
- </div>
904
  </div>
905
- )}
 
906
 
907
  {chatMode === "quiz" &&
908
  message.id === "quiz-1" &&
@@ -911,10 +884,7 @@ export function ChatArea({
911
  !quizState.waitingForAnswer &&
912
  !isAppTyping && (
913
  <div className="flex justify-center py-4">
914
- <Button
915
- onClick={onStartQuiz}
916
- className="bg-red-500 hover:bg-red-600 text-white"
917
- >
918
  Start Quiz
919
  </Button>
920
  </div>
@@ -925,11 +895,7 @@ export function ChatArea({
925
  {isAppTyping && (
926
  <div className="flex gap-2 justify-start px-4">
927
  <div className="w-10 h-10 rounded-full overflow-hidden bg-white flex items-center justify-center flex-shrink-0">
928
- <img
929
- src={clareAvatar}
930
- alt="Clare"
931
- className="w-full h-full object-cover"
932
- />
933
  </div>
934
  <div className="bg-muted rounded-2xl px-4 py-3">
935
  <div className="flex gap-1">
@@ -972,12 +938,9 @@ export function ChatArea({
972
  )}
973
 
974
  {/* Composer */}
975
- <div
976
- ref={composerRef}
977
- className="flex-shrink-0 bg-background/95 backdrop-blur-sm z-20 border-t border-border"
978
- >
979
  <div className="max-w-4xl mx-auto px-4 py-4">
980
- {/* Uploaded Files Preview (chip UI) */}
981
  {(uploadedFiles.length > 0 || pendingFiles.length > 0) && (
982
  <div className="mb-2 flex flex-wrap gap-2 max-h-32 overflow-y-auto">
983
  {/* uploaded */}
@@ -985,22 +948,18 @@ export function ChatArea({
985
  const key = `${uf.file.name}::${uf.file.size}::${uf.file.lastModified}`;
986
 
987
  const nameLower = uf.file.name.toLowerCase();
988
-
989
  const isImage = [".jpg", ".jpeg", ".png", ".gif", ".webp"].some((e) =>
990
  nameLower.endsWith(e)
991
  );
992
- const isPdf = ext.endsWith(".pdf");
993
- const isPpt = ext.endsWith(".ppt") || ext.endsWith(".pptx");
994
- const thumbUrl = isImage ? getOrCreate(uf.file) : null;
995
-
996
- // 非图片:选择对应 icon
997
  const fileIcon = isPdf ? pdfIcon : isPpt ? pptIcon : otherIcon;
998
-
 
 
999
  return (
1000
- <div
1001
- key={key}
1002
- className="flex items-center justify-between gap-2 rounded-md border px-3 py-2"
1003
- >
1004
  {/* ✅ Thumbnail (image preview or file icon) */}
1005
  <div className="h-10 w-10 flex-shrink-0 rounded-lg overflow-hidden border border-border bg-muted">
1006
  {isImage ? (
@@ -1025,18 +984,13 @@ export function ChatArea({
1025
  />
1026
  )}
1027
  </div>
1028
-
1029
  <div className="min-w-0 flex-1">
1030
  <div className="truncate text-sm font-medium">{uf.file.name}</div>
1031
  <div className="text-xs text-muted-foreground">{uf.type}</div>
1032
  </div>
1033
-
1034
- <Button
1035
- variant="ghost"
1036
- size="icon"
1037
- onClick={() => onRemoveFile(i)}
1038
- title="Remove"
1039
- >
1040
  <Trash2 className="h-4 w-4" />
1041
  </Button>
1042
  </div>
 
125
 
126
  onReviewActivity?: (event: ReviewEventType) => void;
127
  currentUserId?: string; // backend user_id
128
+ docType?: string; // backend doc_type (optional)
129
  }
130
 
131
  interface PendingFile {
 
133
  type: FileType;
134
  }
135
 
 
 
 
 
 
 
 
 
 
 
136
  export function ChatArea({
137
  messages,
138
  onSendMessage,
 
258
  if (messages.length > previousMessagesLength.current) {
259
  const el = scrollContainerRef.current;
260
  if (el) {
261
+ const nearBottom = el.scrollHeight - el.scrollTop - el.clientHeight < 240;
 
262
  if (nearBottom) scrollToBottom("smooth");
263
  }
264
  }
 
333
  const buildPreviewContent = () => {
334
  if (!messages.length) return "";
335
  return messages
336
+ .map((msg) => `${msg.role === "user" ? "You" : "Clare"}: ${msg.content}`)
 
 
 
337
  .join("\n\n");
338
  };
339
 
 
351
  summary += `Key Points:\n`;
352
  userMessages.slice(0, 3).forEach((msg, idx) => {
353
  const preview = msg.content.substring(0, 80);
354
+ summary += `${idx + 1}. ${preview}${msg.content.length > 80 ? "..." : ""}\n`;
 
 
355
  });
356
 
357
  return summary;
 
484
  const saved = isCurrentChatSaved();
485
 
486
  if (saved) {
487
+ // keep behavior
488
  onConfirmClear(false as any);
489
  return;
490
  }
 
542
  });
543
 
544
  if (validFiles.length > 0) {
545
+ setPendingFiles(validFiles.map((file) => ({ file, type: "other" as FileType })));
 
 
546
  setShowTypeDialog(true);
547
  } else {
548
  toast.error("Please upload .pdf, .docx, .pptx, or image files");
 
569
  });
570
 
571
  if (validFiles.length > 0) {
572
+ setPendingFiles(validFiles.map((file) => ({ file, type: "other" as FileType })));
 
 
573
  setShowTypeDialog(true);
574
  } else {
575
  toast.error("Please upload .pdf, .docx, .pptx, or image files");
 
600
  };
601
 
602
  const handlePendingFileTypeChange = (index: number, type: FileType) => {
603
+ setPendingFiles((prev) => prev.map((pf, i) => (i === index ? { ...pf, type } : pf)));
 
 
604
  };
605
 
606
  // File helpers
 
609
  if (ext.endsWith(".pdf")) return FileText;
610
  if (ext.endsWith(".docx") || ext.endsWith(".doc")) return File;
611
  if (ext.endsWith(".pptx") || ext.endsWith(".ppt")) return Presentation;
612
+ if ([".jpg", ".jpeg", ".png", ".gif", ".webp"].some((e) => ext.endsWith(e))) return ImageIcon;
 
613
  return File;
614
  };
615
 
 
621
 
622
  const fileKey = (f: File) => `${f.name}::${f.size}::${f.lastModified}`;
623
 
624
+ // useObjectUrlCache: for image thumbnails (uploaded + pending)
625
  const allThumbFiles = useMemo(() => {
626
  return [...uploadedFiles.map((u) => u.file), ...pendingFiles.map((p) => p.file)];
627
  }, [uploadedFiles, pendingFiles]);
628
 
629
+ const { getOrCreate } = useObjectUrlCache(allThumbFiles);
630
 
631
+ // NEW: a compact "chip" UI (the one with left X)
632
  const FileChip = ({
633
  file,
634
  index,
 
641
  const ext = file.name.toLowerCase();
642
  const isImage = [".jpg", ".jpeg", ".png", ".gif", ".webp"].some((e) => ext.endsWith(e));
643
 
644
+ const isPdf = ext.endsWith(".pdf");
645
+ const isPpt = ext.endsWith(".ppt") || ext.endsWith(".pptx");
646
+ const fileIcon = isPdf ? pdfIcon : isPpt ? pptIcon : otherIcon;
647
+
648
+ const label = isPdf
649
  ? "PDF"
650
+ : isPpt
651
  ? "Presentation"
652
  : ext.endsWith(".docx") || ext.endsWith(".doc")
653
  ? "Document"
 
684
  <div className="text-xs text-muted-foreground">{label}</div>
685
  </div>
686
 
687
+ {/* Thumbnail (image preview or file icon) */}
688
+ <div className="relative h-10 w-10 flex-shrink-0 rounded-lg overflow-hidden border border-border bg-muted">
689
+ {isImage ? (
690
+ thumbUrl ? (
691
  <img
692
  src={thumbUrl}
693
  alt={file.name}
 
698
  <div className="h-full w-full flex items-center justify-center">
699
  <ImageIcon className="h-4 w-4 text-muted-foreground" />
700
  </div>
701
+ )
702
+ ) : (
703
+ <img
704
+ src={fileIcon}
705
+ alt={file.name}
706
+ className="h-full w-full object-contain p-1"
707
+ draggable={false}
708
+ />
709
+ )}
710
+ </div>
711
  </div>
712
  );
713
  };
 
802
  size="icon"
803
  onClick={handleSaveClick}
804
  disabled={!isLoggedIn}
805
+ className={`h-8 w-8 rounded-md hover:bg-muted/50 ${isCurrentChatSaved() ? "text-primary" : ""}`}
 
 
806
  title={isCurrentChatSaved() ? "Unsave" : "Save"}
807
  >
808
+ <Bookmark className={`h-4 w-4 ${isCurrentChatSaved() ? "fill-primary text-primary" : ""}`} />
 
 
 
 
809
  </Button>
810
 
811
  <Button
 
857
  message={message}
858
  showSenderInfo={spaceType === "group"}
859
  isFirstGreeting={
860
+ (message.id === "1" || message.id === "review-1" || message.id === "quiz-1") &&
 
 
861
  message.role === "assistant"
862
  }
863
  showNextButton={message.showNextButton && !isAppTyping}
 
868
  docType={docType}
869
  />
870
 
871
+ {chatMode === "review" && message.id === "review-1" && message.role === "assistant" && (
872
+ <div className="flex gap-2 justify-start px-4">
873
+ <div className="w-10 h-10 flex-shrink-0" />
874
+ <div className="w-full" style={{ maxWidth: "min(770px, calc(100% - 2rem))" }}>
875
+ <SmartReview onReviewTopic={handleReviewTopic} onReviewAll={handleReviewAll} />
 
 
 
 
 
 
 
 
 
 
 
876
  </div>
877
+ </div>
878
+ )}
879
 
880
  {chatMode === "quiz" &&
881
  message.id === "quiz-1" &&
 
884
  !quizState.waitingForAnswer &&
885
  !isAppTyping && (
886
  <div className="flex justify-center py-4">
887
+ <Button onClick={onStartQuiz} className="bg-red-500 hover:bg-red-600 text-white">
 
 
 
888
  Start Quiz
889
  </Button>
890
  </div>
 
895
  {isAppTyping && (
896
  <div className="flex gap-2 justify-start px-4">
897
  <div className="w-10 h-10 rounded-full overflow-hidden bg-white flex items-center justify-center flex-shrink-0">
898
+ <img src={clareAvatar} alt="Clare" className="w-full h-full object-cover" />
 
 
 
 
899
  </div>
900
  <div className="bg-muted rounded-2xl px-4 py-3">
901
  <div className="flex gap-1">
 
938
  )}
939
 
940
  {/* Composer */}
941
+ <div ref={composerRef} className="flex-shrink-0 bg-background/95 backdrop-blur-sm z-20 border-t border-border">
 
 
 
942
  <div className="max-w-4xl mx-auto px-4 py-4">
943
+ {/* Uploaded Files Preview */}
944
  {(uploadedFiles.length > 0 || pendingFiles.length > 0) && (
945
  <div className="mb-2 flex flex-wrap gap-2 max-h-32 overflow-y-auto">
946
  {/* uploaded */}
 
948
  const key = `${uf.file.name}::${uf.file.size}::${uf.file.lastModified}`;
949
 
950
  const nameLower = uf.file.name.toLowerCase();
 
951
  const isImage = [".jpg", ".jpeg", ".png", ".gif", ".webp"].some((e) =>
952
  nameLower.endsWith(e)
953
  );
954
+
955
+ const isPdf = nameLower.endsWith(".pdf");
956
+ const isPpt = nameLower.endsWith(".ppt") || nameLower.endsWith(".pptx");
 
 
957
  const fileIcon = isPdf ? pdfIcon : isPpt ? pptIcon : otherIcon;
958
+
959
+ const thumbUrl = isImage ? getOrCreate(uf.file) : null;
960
+
961
  return (
962
+ <div key={key} className="flex items-center justify-between gap-2 rounded-md border px-3 py-2">
 
 
 
963
  {/* ✅ Thumbnail (image preview or file icon) */}
964
  <div className="h-10 w-10 flex-shrink-0 rounded-lg overflow-hidden border border-border bg-muted">
965
  {isImage ? (
 
984
  />
985
  )}
986
  </div>
987
+
988
  <div className="min-w-0 flex-1">
989
  <div className="truncate text-sm font-medium">{uf.file.name}</div>
990
  <div className="text-xs text-muted-foreground">{uf.type}</div>
991
  </div>
992
+
993
+ <Button variant="ghost" size="icon" onClick={() => onRemoveFile(i)} title="Remove">
 
 
 
 
 
994
  <Trash2 className="h-4 w-4" />
995
  </Button>
996
  </div>