Spaces:
Sleeping
Sleeping
Update web/src/components/ChatArea.tsx
Browse files- 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
|
| 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 |
-
|
|
|
|
| 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 <
|
| 216 |
setShowScrollButton(!isAtBottom);
|
| 217 |
setShowTopBorder(scrollTop > 0);
|
| 218 |
};
|
|
@@ -733,395 +737,424 @@ export function ChatArea({
|
|
| 733 |
);
|
| 734 |
};
|
| 735 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 736 |
return (
|
| 737 |
-
// IMPORTANT:
|
| 738 |
<div className="relative flex flex-col h-full min-h-0 overflow-hidden">
|
| 739 |
-
|
| 740 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 741 |
<div
|
| 742 |
-
|
| 743 |
-
|
| 744 |
-
|
|
|
|
| 745 |
>
|
| 746 |
-
{/*
|
| 747 |
-
<div
|
| 748 |
-
|
| 749 |
-
|
| 750 |
-
|
| 751 |
-
|
| 752 |
-
|
| 753 |
-
|
| 754 |
-
|
| 755 |
-
|
| 756 |
-
|
| 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 |
-
|
| 769 |
-
|
| 770 |
-
|
| 771 |
-
|
| 772 |
-
|
| 773 |
-
|
| 774 |
-
|
| 775 |
-
|
| 776 |
-
|
| 777 |
-
|
| 778 |
-
|
| 779 |
-
|
| 780 |
-
|
| 781 |
-
|
| 782 |
-
|
| 783 |
-
|
| 784 |
-
|
| 785 |
-
|
| 786 |
-
|
| 787 |
-
|
| 788 |
-
|
| 789 |
-
|
| 790 |
-
|
| 791 |
-
|
| 792 |
-
|
| 793 |
-
<
|
| 794 |
-
|
| 795 |
-
|
| 796 |
-
|
| 797 |
-
|
| 798 |
-
|
| 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 |
-
|
| 818 |
-
|
| 819 |
-
|
| 820 |
-
|
| 821 |
-
|
| 822 |
-
|
| 823 |
-
|
| 824 |
-
|
| 825 |
-
|
| 826 |
-
|
| 827 |
-
|
| 828 |
-
|
| 829 |
|
| 830 |
-
|
| 831 |
-
|
| 832 |
-
|
| 833 |
-
|
| 834 |
-
|
| 835 |
-
|
| 836 |
-
|
| 837 |
-
|
| 838 |
-
|
| 839 |
-
|
| 840 |
|
| 841 |
-
|
| 842 |
-
|
| 843 |
-
|
| 844 |
-
|
| 845 |
-
|
| 846 |
-
|
| 847 |
-
|
| 848 |
-
|
| 849 |
-
|
| 850 |
-
|
| 851 |
|
| 852 |
-
|
| 853 |
-
|
| 854 |
-
|
| 855 |
-
|
| 856 |
-
|
| 857 |
-
|
| 858 |
-
|
| 859 |
-
|
| 860 |
-
|
| 861 |
-
|
| 862 |
-
</div>
|
| 863 |
</div>
|
|
|
|
| 864 |
|
| 865 |
-
|
| 866 |
-
|
| 867 |
-
|
| 868 |
-
|
| 869 |
-
|
| 870 |
-
|
| 871 |
-
|
| 872 |
-
|
| 873 |
-
|
| 874 |
-
|
| 875 |
-
|
| 876 |
-
|
| 877 |
-
|
| 878 |
-
|
| 879 |
-
|
| 880 |
-
|
| 881 |
|
| 882 |
-
|
| 883 |
-
|
| 884 |
-
|
| 885 |
-
|
| 886 |
-
|
| 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 |
-
|
| 912 |
-
|
| 913 |
-
|
| 914 |
-
|
| 915 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 916 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 917 |
</div>
|
| 918 |
</div>
|
| 919 |
-
|
|
|
|
| 920 |
|
| 921 |
-
|
| 922 |
-
|
| 923 |
</div>
|
| 924 |
</div>
|
|
|
|
| 925 |
|
| 926 |
-
|
| 927 |
-
|
| 928 |
-
|
| 929 |
-
|
| 930 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 931 |
>
|
| 932 |
-
<
|
| 933 |
-
|
| 934 |
-
|
| 935 |
-
|
| 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 |
-
|
| 1070 |
-
|
| 1071 |
-
|
| 1072 |
-
|
| 1073 |
-
|
| 1074 |
-
|
| 1075 |
-
|
| 1076 |
-
|
| 1077 |
-
|
| 1078 |
-
|
| 1079 |
-
|
| 1080 |
-
|
|
|
|
|
|
|
| 1081 |
|
| 1082 |
-
|
| 1083 |
-
|
| 1084 |
-
|
| 1085 |
-
|
| 1086 |
-
|
| 1087 |
-
|
| 1088 |
-
|
| 1089 |
-
|
| 1090 |
-
|
| 1091 |
-
|
| 1092 |
-
|
| 1093 |
-
|
| 1094 |
-
|
| 1095 |
-
|
| 1096 |
-
|
| 1097 |
-
|
| 1098 |
-
|
| 1099 |
-
|
| 1100 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1101 |
disabled={!isLoggedIn || (chatMode === "quiz" && !quizState.waitingForAnswer)}
|
| 1102 |
-
className=
|
| 1103 |
-
|
| 1104 |
-
|
| 1105 |
-
|
|
|
|
|
|
|
|
|
|
| 1106 |
|
| 1107 |
-
|
| 1108 |
-
|
| 1109 |
-
|
| 1110 |
-
|
| 1111 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1112 |
|
| 1113 |
-
|
| 1114 |
-
|
| 1115 |
-
type="
|
| 1116 |
-
|
| 1117 |
-
|
| 1118 |
-
|
| 1119 |
-
|
| 1120 |
-
|
| 1121 |
-
|
| 1122 |
</div>
|
| 1123 |
-
|
| 1124 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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">
|
|
|
|
|
|
|
| 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 "
|
|
|
|
|
|
|
| 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">
|
|
|
|
|
|
|
| 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
|
|
|
|
|
|
|
|
|
|
| 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'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 "
|
| 1319 |
+
{fileToDelete !== null ? uploadedFiles[fileToDelete]?.file.name : ""}
|
| 1320 |
+
"? 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>
|