SarahXia0405 commited on
Commit
fd26e77
·
verified ·
1 Parent(s): 7e1caca

Update web/src/components/ChatArea.tsx

Browse files
Files changed (1) hide show
  1. web/src/components/ChatArea.tsx +12 -302
web/src/components/ChatArea.tsx CHANGED
@@ -86,7 +86,8 @@ interface ChatAreaProps {
86
  uploadedFiles: UploadedFile[];
87
  onFileUpload: (files: File[]) => void;
88
  onRemoveFile: (index: number) => void;
89
- onProfileBioUpdate?: (bio: string) => void; // ✅ NEW
 
90
 
91
  onFileTypeChange: (index: number, type: FileType) => void;
92
  memoryProgress: number;
@@ -174,7 +175,6 @@ function FileViewerContent({ file }: { file: File }) {
174
  }
175
 
176
  if (isPdfFile(file.name)) {
177
- // Force PDF MIME, in case file.type is empty and the browser blocks preview.
178
  const pdfBlob = new Blob([file], { type: "application/pdf" });
179
  const pdfUrl = URL.createObjectURL(pdfBlob);
180
 
@@ -296,71 +296,6 @@ export function ChatArea({
296
  const [shareLink, setShareLink] = useState("");
297
  const [targetWorkspaceId, setTargetWorkspaceId] = useState<string>("");
298
 
299
- // --------------------------
300
- // Profile Init Flow (Ask mode)
301
- // --------------------------
302
- type InitStatus = "idle" | "offered" | "asking" | "generating" | "done";
303
-
304
- type InitQ = {
305
- id: string;
306
- title: string;
307
- placeholder?: string;
308
- };
309
-
310
- const INIT_QUESTIONS: InitQ[] = [
311
- {
312
- id: "course_goal",
313
- title: "What’s the single most important outcome you want from this course?",
314
- placeholder: "e.g., understand LLM basics, build a project, prep for an exam, apply to work…",
315
- },
316
- {
317
- id: "background",
318
- title: "What’s your current background (major, job, or anything relevant)?",
319
- placeholder: "One sentence is totally fine.",
320
- },
321
- {
322
- id: "ai_experience",
323
- title: "Have you worked with AI/LLMs before? If yes, at what level?",
324
- placeholder: "e.g., none / used ChatGPT / built small projects / research…",
325
- },
326
- {
327
- id: "python_level",
328
- title: "How comfortable are you with Python? (Beginner / Intermediate / Advanced)",
329
- placeholder: "Type one: Beginner / Intermediate / Advanced",
330
- },
331
- {
332
- id: "preferred_format",
333
- title: "What helps you learn best? (You can list multiple, separated by commas)",
334
- placeholder: "Step-by-step, examples, visuals, concise answers, Socratic questions…",
335
- },
336
- {
337
- id: "pace",
338
- title: "What pace do you prefer from me? (Fast / Steady / Very detailed)",
339
- placeholder: "Type one: Fast / Steady / Very detailed",
340
- },
341
- {
342
- id: "biggest_pain",
343
- title: "Where do you typically get stuck when learning technical topics?",
344
- placeholder: "Concepts, tools, task breakdown, math, confidence, time management…",
345
- },
346
- {
347
- id: "support_pref",
348
- title: "When you’re unsure, how should I support you?",
349
- placeholder: "Hints first / guided questions / direct answer / ask then answer…",
350
- },
351
- ];
352
-
353
- const [initStatus, setInitStatus] = useState<InitStatus>("idle");
354
- const [initNeedOffer, setInitNeedOffer] = useState(false);
355
- const [initStep, setInitStep] = useState(0);
356
- const [initAnswers, setInitAnswers] = useState<Record<string, any>>({});
357
- const [generatedBio, setGeneratedBio] = useState<string>("");
358
-
359
- // IMPORTANT: allow typing during "asking"; lock typing only during "generating"
360
- const initInputLocked = chatMode === "ask" && initStatus === "generating";
361
- // Use this to block other actions (uploads / drag-drop / etc.) during asking+generating
362
- const initBlockActions = chatMode === "ask" && (initStatus === "asking" || initStatus === "generating");
363
-
364
  const courses =
365
  availableCourses.length > 0
366
  ? availableCourses
@@ -444,96 +379,10 @@ export function ChatArea({
444
  return () => container.removeEventListener("scroll", handleScroll);
445
  }, []);
446
 
447
- // Check if we should run profile init flow (Ask mode only)
448
- useEffect(() => {
449
- if (!isLoggedIn) return;
450
- if (chatMode !== "ask") return;
451
- if (!currentUserId) return;
452
-
453
- // If already completed in this session, do nothing
454
- if (initStatus !== "idle") return;
455
-
456
- let cancelled = false;
457
-
458
- (async () => {
459
- try {
460
- const r = await fetch(`/api/profile/status?user_id=${encodeURIComponent(currentUserId)}`);
461
- if (!r.ok) return;
462
- const j = await r.json();
463
- if (cancelled) return;
464
-
465
- if (j?.need_init) {
466
- setInitNeedOffer(true);
467
- setInitStatus("offered");
468
- }
469
- } catch {
470
- // ignore
471
- }
472
- })();
473
-
474
- return () => {
475
- cancelled = true;
476
- };
477
- }, [isLoggedIn, chatMode, currentUserId, initStatus]);
478
-
479
  const handleSubmit = async (e: React.FormEvent | React.KeyboardEvent) => {
480
  e.preventDefault();
481
  if (!isLoggedIn) return;
482
 
483
- // INIT FLOW: treat input as the answer to the current init question
484
- if (chatMode === "ask" && initStatus === "asking") {
485
- const text = input.trim();
486
- if (!text) return;
487
-
488
- const q = INIT_QUESTIONS[initStep];
489
- const nextAnswers = { ...initAnswers, [q.id]: text };
490
-
491
- setInitAnswers(nextAnswers);
492
- setInput("");
493
-
494
- const nextStep = initStep + 1;
495
-
496
- // finished -> generate bio + save to backend
497
- if (nextStep >= INIT_QUESTIONS.length) {
498
- setInitStatus("generating");
499
-
500
- try {
501
- const r = await fetch("/api/profile/init_submit", {
502
- method: "POST",
503
- headers: { "Content-Type": "application/json" },
504
- body: JSON.stringify({
505
- user_id: currentUserId,
506
- answers: nextAnswers,
507
- language_preference: "English",
508
- }),
509
- });
510
-
511
- if (!r.ok) throw new Error("init_submit failed");
512
- const j = await r.json();
513
-
514
- setGeneratedBio(j?.bio || "");
515
- onProfileBioUpdate?.(j?.bio || ""); // ✅ NEW: sync into user profile
516
-
517
- setInitStatus("done");
518
- setInitNeedOffer(false);
519
-
520
- // reset
521
- setInitStep(0);
522
- setInitAnswers({});
523
- } catch {
524
- toast.error("Sorry — I couldn’t generate your Bio. Please try again.");
525
- setInitStatus("asking");
526
- }
527
-
528
- return;
529
- }
530
-
531
- // go next question
532
- setInitStep(nextStep);
533
- return;
534
- }
535
-
536
- // ORIGINAL behavior (unchanged)
537
  const hasText = !!input.trim();
538
  const hasFiles = uploadedFiles.length > 0;
539
 
@@ -744,7 +593,6 @@ export function ChatArea({
744
  const saved = isCurrentChatSaved();
745
 
746
  if (saved) {
747
- // keep behavior
748
  onConfirmClear(false as any);
749
  return;
750
  }
@@ -763,7 +611,6 @@ export function ChatArea({
763
  e.preventDefault();
764
  e.stopPropagation();
765
  if (!isLoggedIn) return;
766
- if (initBlockActions) return; // block drag UI during init
767
  setIsDragging(true);
768
  };
769
 
@@ -780,7 +627,6 @@ export function ChatArea({
780
  e.stopPropagation();
781
  setIsDragging(false);
782
  if (!isLoggedIn) return;
783
- if (initBlockActions) return; // block file drop during init
784
 
785
  const fileList = e.dataTransfer.files;
786
  const files: File[] = [];
@@ -810,7 +656,6 @@ export function ChatArea({
810
  return;
811
  }
812
 
813
- // limit total files per conversation
814
  const currentCount = uploadedFiles.length + pendingFiles.length;
815
  const remaining = MAX_UPLOAD_FILES - currentCount;
816
 
@@ -826,7 +671,6 @@ export function ChatArea({
826
  toast.warning(`Only the first ${accepted.length} file(s) were added (max ${MAX_UPLOAD_FILES}).`);
827
  }
828
 
829
- // append, do NOT overwrite existing pending
830
  setPendingFiles((prev) => [
831
  ...prev,
832
  ...accepted.map((file) => ({ file, type: "other" as FileType })),
@@ -836,11 +680,6 @@ export function ChatArea({
836
  };
837
 
838
  const handleFileSelect = (e: React.ChangeEvent<HTMLInputElement>) => {
839
- if (initBlockActions) {
840
- e.target.value = "";
841
- return;
842
- }
843
-
844
  const files = Array.from(e.target.files || []) as File[];
845
 
846
  if (files.length === 0) {
@@ -870,7 +709,6 @@ export function ChatArea({
870
  return;
871
  }
872
 
873
- // limit total files per conversation
874
  const currentCount = uploadedFiles.length + pendingFiles.length;
875
  const remaining = MAX_UPLOAD_FILES - currentCount;
876
 
@@ -887,7 +725,6 @@ export function ChatArea({
887
  toast.warning(`Only the first ${accepted.length} file(s) were added (max ${MAX_UPLOAD_FILES}).`);
888
  }
889
 
890
- // append, do NOT overwrite existing pending
891
  setPendingFiles((prev) => [
892
  ...prev,
893
  ...accepted.map((file) => ({ file, type: "other" as FileType })),
@@ -922,7 +759,6 @@ export function ChatArea({
922
  setPendingFiles((prev) => prev.map((pf, i) => (i === index ? { ...pf, type } : pf)));
923
  };
924
 
925
- // File helpers
926
  const getFileIcon = (filename: string) => {
927
  const ext = filename.toLowerCase();
928
  if (ext.endsWith(".pdf")) return FileText;
@@ -940,14 +776,12 @@ export function ChatArea({
940
 
941
  const fileKey = (f: File) => `${f.name}::${f.size}::${f.lastModified}`;
942
 
943
- // useObjectUrlCache: for image thumbnails (uploaded + pending)
944
  const allThumbFiles = useMemo(() => {
945
  return [...uploadedFiles.map((u) => u.file), ...pendingFiles.map((p) => p.file)];
946
  }, [uploadedFiles, pendingFiles]);
947
 
948
  const { getOrCreate } = useObjectUrlCache(allThumbFiles);
949
 
950
- // a compact "chip" UI (the one with left Trash)
951
  const FileChip = ({
952
  file,
953
  index,
@@ -1003,7 +837,6 @@ export function ChatArea({
1003
  <div className="text-xs text-muted-foreground">{label}</div>
1004
  </div>
1005
 
1006
- {/* Thumbnail (image preview or file icon) */}
1007
  <div className="relative h-10 w-10 flex-shrink-0 rounded-lg overflow-hidden border border-border bg-muted">
1008
  {isImage ? (
1009
  thumbUrl ? (
@@ -1033,11 +866,6 @@ export function ChatArea({
1033
 
1034
  const bottomPad = Math.max(24, composerHeight + 24);
1035
 
1036
- const initPlaceholder =
1037
- initStatus === "asking"
1038
- ? "Type your answer here and press Enter / Send..."
1039
- : undefined;
1040
-
1041
  return (
1042
  <div className="relative flex flex-col h-full min-h-0 w-full overflow-hidden">
1043
  {/* Top Bar */}
@@ -1125,7 +953,7 @@ export function ChatArea({
1125
  variant="ghost"
1126
  size="icon"
1127
  onClick={handleSaveClick}
1128
- disabled={!isLoggedIn || initBlockActions}
1129
  className={`h-8 w-8 rounded-md hover:bg-muted/50 ${isCurrentChatSaved() ? "text-primary" : ""}`}
1130
  title={isCurrentChatSaved() ? "Unsave" : "Save"}
1131
  >
@@ -1136,7 +964,7 @@ export function ChatArea({
1136
  variant="ghost"
1137
  size="icon"
1138
  onClick={handleOpenDownloadDialog}
1139
- disabled={!isLoggedIn || initBlockActions}
1140
  className="h-8 w-8 rounded-md hover:bg-muted/50"
1141
  title="Download"
1142
  >
@@ -1147,7 +975,7 @@ export function ChatArea({
1147
  variant="ghost"
1148
  size="icon"
1149
  onClick={handleShareClick}
1150
- disabled={!isLoggedIn || initBlockActions}
1151
  className="h-8 w-8 rounded-md hover:bg-muted/50"
1152
  title="Share"
1153
  >
@@ -1157,7 +985,7 @@ export function ChatArea({
1157
  <Button
1158
  variant="outline"
1159
  onClick={handleClearClick}
1160
- disabled={!isLoggedIn || initBlockActions}
1161
  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)]"
1162
  title="New Chat"
1163
  >
@@ -1216,118 +1044,6 @@ export function ChatArea({
1216
  </React.Fragment>
1217
  ))}
1218
 
1219
- {/* Profile Init Offer (Ask mode only) */}
1220
- {chatMode === "ask" && initNeedOffer && initStatus === "offered" && (
1221
- <div className="flex gap-2 justify-start px-4">
1222
- <div className="w-10 h-10 rounded-full overflow-hidden bg-white flex items-center justify-center flex-shrink-0">
1223
- <img src={clareAvatar} alt="Clare" className="w-full h-full object-cover" />
1224
- </div>
1225
-
1226
- <div className="w-full" style={{ maxWidth: "min(770px, calc(100% - 2rem))" }}>
1227
- <div className="rounded-2xl border bg-card px-4 py-3 space-y-3">
1228
- <div className="font-semibold">Quick intro so I can personalize your experience</div>
1229
- <div className="text-sm text-muted-foreground leading-relaxed">
1230
- I’m Clare, your AI teaching assistant. If you’d like, we can answer a few short questions so I can
1231
- tailor explanations, pacing, and practice to you. Your answers will be summarized into your Profile Bio
1232
- and used only inside this platform.
1233
- </div>
1234
- <div className="flex gap-2">
1235
- <Button
1236
- type="button"
1237
- onClick={() => {
1238
- setInitStatus("asking");
1239
- setInitStep(0);
1240
- setInitAnswers({});
1241
- setGeneratedBio("");
1242
- // Keep focus in the composer for immediate typing
1243
- setTimeout(() => {
1244
- const ta = document.querySelector("textarea");
1245
- (ta as HTMLTextAreaElement | null)?.focus?.();
1246
- }, 0);
1247
- }}
1248
- >
1249
- Yes — let’s start
1250
- </Button>
1251
- <Button
1252
- type="button"
1253
- variant="outline"
1254
- onClick={async () => {
1255
- try {
1256
- await fetch("/api/profile/dismiss", {
1257
- method: "POST",
1258
- headers: { "Content-Type": "application/json" },
1259
- body: JSON.stringify({ user_id: currentUserId, days: 7 }),
1260
- });
1261
- } catch {}
1262
- setInitNeedOffer(false);
1263
- setInitStatus("idle");
1264
- }}
1265
- >
1266
- Maybe later
1267
- </Button>
1268
- </div>
1269
- </div>
1270
- </div>
1271
- </div>
1272
- )}
1273
-
1274
- {/* Current init question bubble */}
1275
- {chatMode === "ask" && initStatus === "asking" && (
1276
- <div className="flex gap-2 justify-start px-4">
1277
- <div className="w-10 h-10 rounded-full overflow-hidden bg-white flex items-center justify-center flex-shrink-0">
1278
- <img src={clareAvatar} alt="Clare" className="w-full h-full object-cover" />
1279
- </div>
1280
-
1281
- <div
1282
- className="bg-muted rounded-2xl px-4 py-3 w-full"
1283
- style={{ maxWidth: "min(770px, calc(100% - 2rem))" }}
1284
- >
1285
- <div className="text-sm font-medium mb-1">
1286
- {INIT_QUESTIONS[initStep]?.title}
1287
- </div>
1288
- <div className="text-xs text-muted-foreground">
1289
- {INIT_QUESTIONS[initStep]?.placeholder || "Just type your answer and press Send."}
1290
- </div>
1291
- <div className="text-xs text-muted-foreground mt-2">
1292
- Question {initStep + 1} of {INIT_QUESTIONS.length}
1293
- </div>
1294
- </div>
1295
- </div>
1296
- )}
1297
-
1298
- {/* Generating bubble */}
1299
- {chatMode === "ask" && initStatus === "generating" && (
1300
- <div className="flex gap-2 justify-start px-4">
1301
- <div className="w-10 h-10 rounded-full overflow-hidden bg-white flex items-center justify-center flex-shrink-0">
1302
- <img src={clareAvatar} alt="Clare" className="w-full h-full object-cover" />
1303
- </div>
1304
- <div className="bg-muted rounded-2xl px-4 py-3">
1305
- <div className="text-sm">Thanks — I’m generating your Profile Bio now…</div>
1306
- </div>
1307
- </div>
1308
- )}
1309
-
1310
- {/* Done bubble with bio */}
1311
- {chatMode === "ask" && initStatus === "done" && !!generatedBio && (
1312
- <div className="flex gap-2 justify-start px-4">
1313
- <div className="w-10 h-10 rounded-full overflow-hidden bg-white flex items-center justify-center flex-shrink-0">
1314
- <img src={clareAvatar} alt="Clare" className="w-full h-full object-cover" />
1315
- </div>
1316
-
1317
- <div className="w-full" style={{ maxWidth: "min(770px, calc(100% - 2rem))" }}>
1318
- <div className="bg-muted rounded-2xl px-4 py-3 space-y-2">
1319
- <div className="text-sm font-medium">
1320
- Thank you — I’ve saved this to your Profile Bio.
1321
- </div>
1322
- <div className="text-sm whitespace-pre-wrap">{generatedBio}</div>
1323
- <div className="text-xs text-muted-foreground">
1324
- You can update it anytime. I’ll use this to adapt explanations, pacing, and practice.
1325
- </div>
1326
- </div>
1327
- </div>
1328
- </div>
1329
- )}
1330
-
1331
  {isAppTyping && (
1332
  <div className="flex gap-2 justify-start px-4">
1333
  <div className="w-10 h-10 rounded-full overflow-hidden bg-white flex items-center justify-center flex-shrink-0">
@@ -1413,7 +1129,6 @@ export function ChatArea({
1413
  className="flex items-center justify-between gap-2 rounded-md border px-3 py-2 cursor-pointer hover:bg-muted/40"
1414
  title="Click to preview"
1415
  >
1416
- {/* Thumbnail (image preview or file icon) */}
1417
  <div className="h-10 w-10 flex-shrink-0 rounded-lg overflow-hidden border border-border bg-muted">
1418
  {isImage ? (
1419
  thumbUrl ? (
@@ -1448,11 +1163,10 @@ export function ChatArea({
1448
  size="icon"
1449
  onClick={(e) => {
1450
  e.preventDefault();
1451
- e.stopPropagation(); // don't open viewer
1452
  onRemoveFile(i);
1453
  }}
1454
  title="Remove"
1455
- disabled={initBlockActions}
1456
  >
1457
  <Trash2 className="h-4 w-4" />
1458
  </Button>
@@ -1489,7 +1203,7 @@ export function ChatArea({
1489
  variant="ghost"
1490
  size="sm"
1491
  className="gap-1.5 h-8 px-2 text-xs hover:bg-muted/50"
1492
- disabled={!isLoggedIn || initBlockActions}
1493
  type="button"
1494
  >
1495
  <span>{modeLabels[learningMode]}</span>
@@ -1590,7 +1304,6 @@ export function ChatArea({
1590
  variant="ghost"
1591
  disabled={
1592
  !isLoggedIn ||
1593
- initBlockActions ||
1594
  (chatMode === "quiz" && !quizState.waitingForAnswer)
1595
  }
1596
  className="h-8 w-8 hover:bg-muted/50"
@@ -1606,8 +1319,7 @@ export function ChatArea({
1606
  onChange={(e) => setInput(e.target.value)}
1607
  onKeyDown={handleKeyDown}
1608
  placeholder={
1609
- initPlaceholder ??
1610
- (!isLoggedIn
1611
  ? "Please log in on the right to start chatting..."
1612
  : chatMode === "quiz"
1613
  ? quizState.waitingForAnswer
@@ -1619,11 +1331,10 @@ export function ChatArea({
1619
  ? "Type a message or drag files here... (mention @Clare to get AI assistance)"
1620
  : learningMode === "general"
1621
  ? "Ask me anything! Please provide context about your question..."
1622
- : "Ask Clare anything about the course or drag files here...")
1623
  }
1624
  disabled={
1625
  !isLoggedIn ||
1626
- initInputLocked || // key fix: DO NOT lock input during "asking"
1627
  (chatMode === "quiz" && !quizState.waitingForAnswer)
1628
  }
1629
  className={`min-h-[80px] pl-4 pr-20 resize-none bg-background border-2 ${
@@ -1637,8 +1348,7 @@ export function ChatArea({
1637
  size="icon"
1638
  disabled={
1639
  (!input.trim() && uploadedFiles.length === 0) ||
1640
- !isLoggedIn ||
1641
- initInputLocked
1642
  }
1643
  className="h-8 w-8 rounded-full"
1644
  >
@@ -1653,7 +1363,7 @@ export function ChatArea({
1653
  accept=".pdf,.docx,.pptx,.doc,.ppt,.jpg,.jpeg,.png,.gif,.webp"
1654
  onChange={handleFileSelect}
1655
  className="hidden"
1656
- disabled={!isLoggedIn || initBlockActions}
1657
  />
1658
  </div>
1659
  </form>
 
86
  uploadedFiles: UploadedFile[];
87
  onFileUpload: (files: File[]) => void;
88
  onRemoveFile: (index: number) => void;
89
+
90
+ onProfileBioUpdate?: (bio: string) => void; // still allowed (ProfileEditor / future AI updates)
91
 
92
  onFileTypeChange: (index: number, type: FileType) => void;
93
  memoryProgress: number;
 
175
  }
176
 
177
  if (isPdfFile(file.name)) {
 
178
  const pdfBlob = new Blob([file], { type: "application/pdf" });
179
  const pdfUrl = URL.createObjectURL(pdfBlob);
180
 
 
296
  const [shareLink, setShareLink] = useState("");
297
  const [targetWorkspaceId, setTargetWorkspaceId] = useState<string>("");
298
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
299
  const courses =
300
  availableCourses.length > 0
301
  ? availableCourses
 
379
  return () => container.removeEventListener("scroll", handleScroll);
380
  }, []);
381
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
382
  const handleSubmit = async (e: React.FormEvent | React.KeyboardEvent) => {
383
  e.preventDefault();
384
  if (!isLoggedIn) return;
385
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
386
  const hasText = !!input.trim();
387
  const hasFiles = uploadedFiles.length > 0;
388
 
 
593
  const saved = isCurrentChatSaved();
594
 
595
  if (saved) {
 
596
  onConfirmClear(false as any);
597
  return;
598
  }
 
611
  e.preventDefault();
612
  e.stopPropagation();
613
  if (!isLoggedIn) return;
 
614
  setIsDragging(true);
615
  };
616
 
 
627
  e.stopPropagation();
628
  setIsDragging(false);
629
  if (!isLoggedIn) return;
 
630
 
631
  const fileList = e.dataTransfer.files;
632
  const files: File[] = [];
 
656
  return;
657
  }
658
 
 
659
  const currentCount = uploadedFiles.length + pendingFiles.length;
660
  const remaining = MAX_UPLOAD_FILES - currentCount;
661
 
 
671
  toast.warning(`Only the first ${accepted.length} file(s) were added (max ${MAX_UPLOAD_FILES}).`);
672
  }
673
 
 
674
  setPendingFiles((prev) => [
675
  ...prev,
676
  ...accepted.map((file) => ({ file, type: "other" as FileType })),
 
680
  };
681
 
682
  const handleFileSelect = (e: React.ChangeEvent<HTMLInputElement>) => {
 
 
 
 
 
683
  const files = Array.from(e.target.files || []) as File[];
684
 
685
  if (files.length === 0) {
 
709
  return;
710
  }
711
 
 
712
  const currentCount = uploadedFiles.length + pendingFiles.length;
713
  const remaining = MAX_UPLOAD_FILES - currentCount;
714
 
 
725
  toast.warning(`Only the first ${accepted.length} file(s) were added (max ${MAX_UPLOAD_FILES}).`);
726
  }
727
 
 
728
  setPendingFiles((prev) => [
729
  ...prev,
730
  ...accepted.map((file) => ({ file, type: "other" as FileType })),
 
759
  setPendingFiles((prev) => prev.map((pf, i) => (i === index ? { ...pf, type } : pf)));
760
  };
761
 
 
762
  const getFileIcon = (filename: string) => {
763
  const ext = filename.toLowerCase();
764
  if (ext.endsWith(".pdf")) return FileText;
 
776
 
777
  const fileKey = (f: File) => `${f.name}::${f.size}::${f.lastModified}`;
778
 
 
779
  const allThumbFiles = useMemo(() => {
780
  return [...uploadedFiles.map((u) => u.file), ...pendingFiles.map((p) => p.file)];
781
  }, [uploadedFiles, pendingFiles]);
782
 
783
  const { getOrCreate } = useObjectUrlCache(allThumbFiles);
784
 
 
785
  const FileChip = ({
786
  file,
787
  index,
 
837
  <div className="text-xs text-muted-foreground">{label}</div>
838
  </div>
839
 
 
840
  <div className="relative h-10 w-10 flex-shrink-0 rounded-lg overflow-hidden border border-border bg-muted">
841
  {isImage ? (
842
  thumbUrl ? (
 
866
 
867
  const bottomPad = Math.max(24, composerHeight + 24);
868
 
 
 
 
 
 
869
  return (
870
  <div className="relative flex flex-col h-full min-h-0 w-full overflow-hidden">
871
  {/* Top Bar */}
 
953
  variant="ghost"
954
  size="icon"
955
  onClick={handleSaveClick}
956
+ disabled={!isLoggedIn}
957
  className={`h-8 w-8 rounded-md hover:bg-muted/50 ${isCurrentChatSaved() ? "text-primary" : ""}`}
958
  title={isCurrentChatSaved() ? "Unsave" : "Save"}
959
  >
 
964
  variant="ghost"
965
  size="icon"
966
  onClick={handleOpenDownloadDialog}
967
+ disabled={!isLoggedIn}
968
  className="h-8 w-8 rounded-md hover:bg-muted/50"
969
  title="Download"
970
  >
 
975
  variant="ghost"
976
  size="icon"
977
  onClick={handleShareClick}
978
+ disabled={!isLoggedIn}
979
  className="h-8 w-8 rounded-md hover:bg-muted/50"
980
  title="Share"
981
  >
 
985
  <Button
986
  variant="outline"
987
  onClick={handleClearClick}
988
+ disabled={!isLoggedIn}
989
  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)]"
990
  title="New Chat"
991
  >
 
1044
  </React.Fragment>
1045
  ))}
1046
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1047
  {isAppTyping && (
1048
  <div className="flex gap-2 justify-start px-4">
1049
  <div className="w-10 h-10 rounded-full overflow-hidden bg-white flex items-center justify-center flex-shrink-0">
 
1129
  className="flex items-center justify-between gap-2 rounded-md border px-3 py-2 cursor-pointer hover:bg-muted/40"
1130
  title="Click to preview"
1131
  >
 
1132
  <div className="h-10 w-10 flex-shrink-0 rounded-lg overflow-hidden border border-border bg-muted">
1133
  {isImage ? (
1134
  thumbUrl ? (
 
1163
  size="icon"
1164
  onClick={(e) => {
1165
  e.preventDefault();
1166
+ e.stopPropagation();
1167
  onRemoveFile(i);
1168
  }}
1169
  title="Remove"
 
1170
  >
1171
  <Trash2 className="h-4 w-4" />
1172
  </Button>
 
1203
  variant="ghost"
1204
  size="sm"
1205
  className="gap-1.5 h-8 px-2 text-xs hover:bg-muted/50"
1206
+ disabled={!isLoggedIn}
1207
  type="button"
1208
  >
1209
  <span>{modeLabels[learningMode]}</span>
 
1304
  variant="ghost"
1305
  disabled={
1306
  !isLoggedIn ||
 
1307
  (chatMode === "quiz" && !quizState.waitingForAnswer)
1308
  }
1309
  className="h-8 w-8 hover:bg-muted/50"
 
1319
  onChange={(e) => setInput(e.target.value)}
1320
  onKeyDown={handleKeyDown}
1321
  placeholder={
1322
+ !isLoggedIn
 
1323
  ? "Please log in on the right to start chatting..."
1324
  : chatMode === "quiz"
1325
  ? quizState.waitingForAnswer
 
1331
  ? "Type a message or drag files here... (mention @Clare to get AI assistance)"
1332
  : learningMode === "general"
1333
  ? "Ask me anything! Please provide context about your question..."
1334
+ : "Ask Clare anything about the course or drag files here..."
1335
  }
1336
  disabled={
1337
  !isLoggedIn ||
 
1338
  (chatMode === "quiz" && !quizState.waitingForAnswer)
1339
  }
1340
  className={`min-h-[80px] pl-4 pr-20 resize-none bg-background border-2 ${
 
1348
  size="icon"
1349
  disabled={
1350
  (!input.trim() && uploadedFiles.length === 0) ||
1351
+ !isLoggedIn
 
1352
  }
1353
  className="h-8 w-8 rounded-full"
1354
  >
 
1363
  accept=".pdf,.docx,.pptx,.doc,.ppt,.jpg,.jpeg,.png,.gif,.webp"
1364
  onChange={handleFileSelect}
1365
  className="hidden"
1366
+ disabled={!isLoggedIn}
1367
  />
1368
  </div>
1369
  </form>