Spaces:
Sleeping
Sleeping
Update web/src/components/ChatArea.tsx
Browse files- web/src/components/ChatArea.tsx +138 -16
web/src/components/ChatArea.tsx
CHANGED
|
@@ -133,6 +133,75 @@ interface PendingFile {
|
|
| 133 |
type: FileType;
|
| 134 |
}
|
| 135 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 136 |
export function ChatArea({
|
| 137 |
messages,
|
| 138 |
onSendMessage,
|
|
@@ -959,7 +1028,24 @@ export function ChatArea({
|
|
| 959 |
const thumbUrl = isImage ? getOrCreate(uf.file) : null;
|
| 960 |
|
| 961 |
return (
|
| 962 |
-
<div
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 ? (
|
|
@@ -990,7 +1076,16 @@ export function ChatArea({
|
|
| 990 |
<div className="text-xs text-muted-foreground">{uf.type}</div>
|
| 991 |
</div>
|
| 992 |
|
| 993 |
-
<Button
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 994 |
<Trash2 className="h-4 w-4" />
|
| 995 |
</Button>
|
| 996 |
</div>
|
|
@@ -1125,10 +1220,7 @@ export function ChatArea({
|
|
| 1125 |
type="button"
|
| 1126 |
size="icon"
|
| 1127 |
variant="ghost"
|
| 1128 |
-
disabled={
|
| 1129 |
-
!isLoggedIn ||
|
| 1130 |
-
(chatMode === "quiz" && !quizState.waitingForAnswer)
|
| 1131 |
-
}
|
| 1132 |
className="h-8 w-8 hover:bg-muted/50"
|
| 1133 |
onClick={() => fileInputRef.current?.click()}
|
| 1134 |
title="Upload files"
|
|
@@ -1156,22 +1248,14 @@ export function ChatArea({
|
|
| 1156 |
? "Ask me anything! Please provide context about your question..."
|
| 1157 |
: "Ask Clare anything about the course or drag files here..."
|
| 1158 |
}
|
| 1159 |
-
disabled={
|
| 1160 |
-
!isLoggedIn ||
|
| 1161 |
-
(chatMode === "quiz" && !quizState.waitingForAnswer)
|
| 1162 |
-
}
|
| 1163 |
className={`min-h-[80px] pl-4 pr-20 resize-none bg-background border-2 ${
|
| 1164 |
isDragging ? "border-primary border-dashed" : "border-border"
|
| 1165 |
}`}
|
| 1166 |
/>
|
| 1167 |
|
| 1168 |
<div className="absolute bottom-2 right-2 flex gap-1">
|
| 1169 |
-
<Button
|
| 1170 |
-
type="submit"
|
| 1171 |
-
size="icon"
|
| 1172 |
-
disabled={!input.trim() || !isLoggedIn}
|
| 1173 |
-
className="h-8 w-8 rounded-full"
|
| 1174 |
-
>
|
| 1175 |
<Send className="h-4 w-4" />
|
| 1176 |
</Button>
|
| 1177 |
</div>
|
|
@@ -1190,6 +1274,44 @@ export function ChatArea({
|
|
| 1190 |
</div>
|
| 1191 |
</div>
|
| 1192 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1193 |
{/* Start New Conversation Confirmation Dialog */}
|
| 1194 |
<AlertDialog open={showClearDialog} onOpenChange={onCancelClear}>
|
| 1195 |
<AlertDialogContent>
|
|
|
|
| 133 |
type: FileType;
|
| 134 |
}
|
| 135 |
|
| 136 |
+
// ✅ NEW: File viewer content (image full preview + pdf iframe; others download)
|
| 137 |
+
function isImageFile(name: string) {
|
| 138 |
+
const n = name.toLowerCase();
|
| 139 |
+
return [".jpg", ".jpeg", ".png", ".gif", ".webp"].some((e) => n.endsWith(e));
|
| 140 |
+
}
|
| 141 |
+
function isPdfFile(name: string) {
|
| 142 |
+
return name.toLowerCase().endsWith(".pdf");
|
| 143 |
+
}
|
| 144 |
+
function isDocFile(name: string) {
|
| 145 |
+
const n = name.toLowerCase();
|
| 146 |
+
return n.endsWith(".doc") || n.endsWith(".docx");
|
| 147 |
+
}
|
| 148 |
+
function isPptFile(name: string) {
|
| 149 |
+
const n = name.toLowerCase();
|
| 150 |
+
return n.endsWith(".ppt") || n.endsWith(".pptx");
|
| 151 |
+
}
|
| 152 |
+
|
| 153 |
+
function FileViewerContent({ file }: { file: File }) {
|
| 154 |
+
const [url, setUrl] = React.useState<string>("");
|
| 155 |
+
|
| 156 |
+
React.useEffect(() => {
|
| 157 |
+
const u = URL.createObjectURL(file);
|
| 158 |
+
setUrl(u);
|
| 159 |
+
return () => URL.revokeObjectURL(u);
|
| 160 |
+
}, [file]);
|
| 161 |
+
|
| 162 |
+
if (isImageFile(file.name)) {
|
| 163 |
+
return (
|
| 164 |
+
<div className="w-full">
|
| 165 |
+
<img
|
| 166 |
+
src={url}
|
| 167 |
+
alt={file.name}
|
| 168 |
+
className="w-full h-auto rounded-lg border"
|
| 169 |
+
draggable={false}
|
| 170 |
+
/>
|
| 171 |
+
</div>
|
| 172 |
+
);
|
| 173 |
+
}
|
| 174 |
+
|
| 175 |
+
if (isPdfFile(file.name)) {
|
| 176 |
+
return (
|
| 177 |
+
<div className="w-full h-[70vh] border rounded-lg overflow-hidden">
|
| 178 |
+
<iframe title={file.name} src={url} className="w-full h-full" />
|
| 179 |
+
</div>
|
| 180 |
+
);
|
| 181 |
+
}
|
| 182 |
+
|
| 183 |
+
const kind = isDocFile(file.name)
|
| 184 |
+
? "Word document"
|
| 185 |
+
: isPptFile(file.name)
|
| 186 |
+
? "PowerPoint"
|
| 187 |
+
: "File";
|
| 188 |
+
|
| 189 |
+
return (
|
| 190 |
+
<div className="space-y-3">
|
| 191 |
+
<div className="text-sm text-muted-foreground">
|
| 192 |
+
Preview is not available for this {kind} format in the browser without conversion.
|
| 193 |
+
</div>
|
| 194 |
+
<a
|
| 195 |
+
href={url}
|
| 196 |
+
download={file.name}
|
| 197 |
+
className="inline-flex items-center justify-center h-9 px-3 rounded-md border hover:bg-muted"
|
| 198 |
+
>
|
| 199 |
+
Download to view
|
| 200 |
+
</a>
|
| 201 |
+
</div>
|
| 202 |
+
);
|
| 203 |
+
}
|
| 204 |
+
|
| 205 |
export function ChatArea({
|
| 206 |
messages,
|
| 207 |
onSendMessage,
|
|
|
|
| 1028 |
const thumbUrl = isImage ? getOrCreate(uf.file) : null;
|
| 1029 |
|
| 1030 |
return (
|
| 1031 |
+
<div
|
| 1032 |
+
key={key}
|
| 1033 |
+
role="button"
|
| 1034 |
+
tabIndex={0}
|
| 1035 |
+
onClick={() => {
|
| 1036 |
+
setSelectedFile({ file: uf.file, index: i });
|
| 1037 |
+
setShowFileViewer(true);
|
| 1038 |
+
}}
|
| 1039 |
+
onKeyDown={(e) => {
|
| 1040 |
+
if (e.key === "Enter" || e.key === " ") {
|
| 1041 |
+
e.preventDefault();
|
| 1042 |
+
setSelectedFile({ file: uf.file, index: i });
|
| 1043 |
+
setShowFileViewer(true);
|
| 1044 |
+
}
|
| 1045 |
+
}}
|
| 1046 |
+
className="flex items-center justify-between gap-2 rounded-md border px-3 py-2 cursor-pointer hover:bg-muted/40"
|
| 1047 |
+
title="Click to preview"
|
| 1048 |
+
>
|
| 1049 |
{/* ✅ Thumbnail (image preview or file icon) */}
|
| 1050 |
<div className="h-10 w-10 flex-shrink-0 rounded-lg overflow-hidden border border-border bg-muted">
|
| 1051 |
{isImage ? (
|
|
|
|
| 1076 |
<div className="text-xs text-muted-foreground">{uf.type}</div>
|
| 1077 |
</div>
|
| 1078 |
|
| 1079 |
+
<Button
|
| 1080 |
+
variant="ghost"
|
| 1081 |
+
size="icon"
|
| 1082 |
+
onClick={(e) => {
|
| 1083 |
+
e.preventDefault();
|
| 1084 |
+
e.stopPropagation(); // ✅ don't open viewer
|
| 1085 |
+
onRemoveFile(i);
|
| 1086 |
+
}}
|
| 1087 |
+
title="Remove"
|
| 1088 |
+
>
|
| 1089 |
<Trash2 className="h-4 w-4" />
|
| 1090 |
</Button>
|
| 1091 |
</div>
|
|
|
|
| 1220 |
type="button"
|
| 1221 |
size="icon"
|
| 1222 |
variant="ghost"
|
| 1223 |
+
disabled={!isLoggedIn || (chatMode === "quiz" && !quizState.waitingForAnswer)}
|
|
|
|
|
|
|
|
|
|
| 1224 |
className="h-8 w-8 hover:bg-muted/50"
|
| 1225 |
onClick={() => fileInputRef.current?.click()}
|
| 1226 |
title="Upload files"
|
|
|
|
| 1248 |
? "Ask me anything! Please provide context about your question..."
|
| 1249 |
: "Ask Clare anything about the course or drag files here..."
|
| 1250 |
}
|
| 1251 |
+
disabled={!isLoggedIn || (chatMode === "quiz" && !quizState.waitingForAnswer)}
|
|
|
|
|
|
|
|
|
|
| 1252 |
className={`min-h-[80px] pl-4 pr-20 resize-none bg-background border-2 ${
|
| 1253 |
isDragging ? "border-primary border-dashed" : "border-border"
|
| 1254 |
}`}
|
| 1255 |
/>
|
| 1256 |
|
| 1257 |
<div className="absolute bottom-2 right-2 flex gap-1">
|
| 1258 |
+
<Button type="submit" size="icon" disabled={!input.trim() || !isLoggedIn} className="h-8 w-8 rounded-full">
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1259 |
<Send className="h-4 w-4" />
|
| 1260 |
</Button>
|
| 1261 |
</div>
|
|
|
|
| 1274 |
</div>
|
| 1275 |
</div>
|
| 1276 |
|
| 1277 |
+
{/* ✅ NEW: File Viewer Dialog */}
|
| 1278 |
+
<Dialog
|
| 1279 |
+
open={showFileViewer}
|
| 1280 |
+
onOpenChange={(open) => {
|
| 1281 |
+
setShowFileViewer(open);
|
| 1282 |
+
if (!open) setSelectedFile(null);
|
| 1283 |
+
}}
|
| 1284 |
+
>
|
| 1285 |
+
<DialogContent className="max-w-4xl max-h-[85vh] flex flex-col overflow-hidden">
|
| 1286 |
+
<DialogHeader className="min-w-0 flex-shrink-0">
|
| 1287 |
+
<DialogTitle
|
| 1288 |
+
className="pr-8 break-words break-all overflow-wrap-anywhere leading-relaxed"
|
| 1289 |
+
style={{
|
| 1290 |
+
wordBreak: "break-all",
|
| 1291 |
+
overflowWrap: "anywhere",
|
| 1292 |
+
maxWidth: "100%",
|
| 1293 |
+
lineHeight: "1.6",
|
| 1294 |
+
}}
|
| 1295 |
+
>
|
| 1296 |
+
{selectedFile?.file.name}
|
| 1297 |
+
</DialogTitle>
|
| 1298 |
+
<DialogDescription>
|
| 1299 |
+
File size: {selectedFile ? formatFileSize(selectedFile.file.size) : ""}
|
| 1300 |
+
</DialogDescription>
|
| 1301 |
+
</DialogHeader>
|
| 1302 |
+
|
| 1303 |
+
<div className="flex-1 min-h-0 overflow-y-auto mt-4">
|
| 1304 |
+
{selectedFile && <FileViewerContent file={selectedFile.file} />}
|
| 1305 |
+
</div>
|
| 1306 |
+
</DialogContent>
|
| 1307 |
+
</Dialog>
|
| 1308 |
+
|
| 1309 |
+
|
| 1310 |
+
|
| 1311 |
+
|
| 1312 |
+
|
| 1313 |
+
|
| 1314 |
+
|
| 1315 |
{/* Start New Conversation Confirmation Dialog */}
|
| 1316 |
<AlertDialog open={showClearDialog} onOpenChange={onCancelClear}>
|
| 1317 |
<AlertDialogContent>
|