Spaces:
Sleeping
Sleeping
Update web/src/components/ChatArea.tsx
Browse files- 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;
|
| 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
|
|
|
|
|
|
|
|
|
|
|
|
|
| 666 |
? "PDF"
|
| 667 |
-
:
|
| 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 |
-
{
|
| 705 |
-
|
| 706 |
-
|
|
|
|
| 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 |
-
|
| 719 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 890 |
-
|
| 891 |
-
<div className="
|
| 892 |
-
<
|
| 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 |
-
{/*
|
| 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 |
-
|
| 993 |
-
const
|
| 994 |
-
const
|
| 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>
|