SarahXia0405 commited on
Commit
0dc8db8
·
verified ·
1 Parent(s): daf8c39

Update web/src/components/ChatArea.tsx

Browse files
Files changed (1) hide show
  1. web/src/components/ChatArea.tsx +154 -295
web/src/components/ChatArea.tsx CHANGED
@@ -133,7 +133,7 @@ interface PendingFile {
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));
@@ -173,7 +173,7 @@ function FileViewerContent({ file }: { file: File }) {
173
  }
174
 
175
  if (isPdfFile(file.name)) {
176
- // Force pdf MIME to avoid browser blocking when file.type is empty
177
  const pdfBlob = new Blob([file], { type: "application/pdf" });
178
  const pdfUrl = URL.createObjectURL(pdfBlob);
179
 
@@ -182,8 +182,7 @@ function FileViewerContent({ file }: { file: File }) {
182
  <object data={pdfUrl} type="application/pdf" className="w-full h-full">
183
  <div className="p-3 space-y-2">
184
  <div className="text-sm text-muted-foreground">
185
- PDF preview is blocked by your browser. Please open it in a new tab
186
- or download.
187
  </div>
188
  <div className="flex gap-2">
189
  <a
@@ -217,8 +216,7 @@ function FileViewerContent({ file }: { file: File }) {
217
  return (
218
  <div className="space-y-3">
219
  <div className="text-sm text-muted-foreground">
220
- Preview is not available for this {kind} format in the browser without
221
- conversion.
222
  </div>
223
  <a
224
  href={url}
@@ -231,61 +229,6 @@ function FileViewerContent({ file }: { file: File }) {
231
  );
232
  }
233
 
234
- // --------------------------
235
- // ✅ Profile Init Flow (Ask mode)
236
- // --------------------------
237
- type InitStatus = "idle" | "offered" | "asking" | "generating" | "done";
238
-
239
- type InitQ = {
240
- id: string;
241
- title: string;
242
- placeholder?: string;
243
- };
244
-
245
- const INIT_QUESTIONS: InitQ[] = [
246
- {
247
- id: "course_goal",
248
- title: "What’s the single most important outcome you want from this course?",
249
- placeholder:
250
- "e.g., understand LLM basics, build a project, prep for an exam, apply to work…",
251
- },
252
- {
253
- id: "background",
254
- title: "What’s your current background (major, job, or anything relevant)?",
255
- placeholder: "One sentence is totally fine.",
256
- },
257
- {
258
- id: "ai_experience",
259
- title: "Have you worked with AI/LLMs before? If yes, at what level?",
260
- placeholder: "e.g., none / used ChatGPT / built small projects / research…",
261
- },
262
- {
263
- id: "python_level",
264
- title: "How comfortable are you with Python? (Beginner / Intermediate / Advanced)",
265
- placeholder: "Type one: Beginner / Intermediate / Advanced",
266
- },
267
- {
268
- id: "preferred_format",
269
- title: "What helps you learn best? (You can list multiple, separated by commas)",
270
- placeholder: "Step-by-step, examples, visuals, concise answers, Socratic questions…",
271
- },
272
- {
273
- id: "pace",
274
- title: "What pace do you prefer from me? (Fast / Steady / Very detailed)",
275
- placeholder: "Type one: Fast / Steady / Very detailed",
276
- },
277
- {
278
- id: "biggest_pain",
279
- title: "Where do you typically get stuck when learning technical topics?",
280
- placeholder: "Concepts, tools, task breakdown, math, confidence, time management…",
281
- },
282
- {
283
- id: "support_pref",
284
- title: "When you’re unsure, how should I support you?",
285
- placeholder: "Hints first / guided questions / direct answer / ask then answer…",
286
- },
287
- ];
288
-
289
  export function ChatArea({
290
  messages,
291
  onSendMessage,
@@ -352,16 +295,69 @@ export function ChatArea({
352
  const [targetWorkspaceId, setTargetWorkspaceId] = useState<string>("");
353
 
354
  // --------------------------
355
- // Profile Init Flow state
356
  // --------------------------
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
357
  const [initStatus, setInitStatus] = useState<InitStatus>("idle");
358
  const [initNeedOffer, setInitNeedOffer] = useState(false);
359
  const [initStep, setInitStep] = useState(0);
360
  const [initAnswers, setInitAnswers] = useState<Record<string, any>>({});
361
  const [generatedBio, setGeneratedBio] = useState<string>("");
362
 
363
- const initLocked =
364
- chatMode === "ask" && (initStatus === "asking" || initStatus === "generating");
 
 
365
 
366
  const courses =
367
  availableCourses.length > 0
@@ -377,7 +373,7 @@ export function ChatArea({
377
  const scrollContainerRef = useRef<HTMLDivElement>(null);
378
  const fileInputRef = useRef<HTMLInputElement>(null);
379
 
380
- // Composer measured height (dynamic) to reserve bottom padding for messages
381
  const composerRef = useRef<HTMLDivElement>(null);
382
  const [composerHeight, setComposerHeight] = useState<number>(160);
383
 
@@ -423,8 +419,7 @@ export function ChatArea({
423
  if (messages.length > previousMessagesLength.current) {
424
  const el = scrollContainerRef.current;
425
  if (el) {
426
- const nearBottom =
427
- el.scrollHeight - el.scrollTop - el.clientHeight < 240;
428
  if (nearBottom) scrollToBottom("smooth");
429
  }
430
  }
@@ -447,33 +442,20 @@ export function ChatArea({
447
  return () => container.removeEventListener("scroll", handleScroll);
448
  }, []);
449
 
450
- // Check if we should run profile init flow (Ask mode only)
451
  useEffect(() => {
452
- // Leaving Ask: reset in-progress init to avoid stale state on other tabs
453
- if (chatMode !== "ask") {
454
- if (initStatus === "asking" || initStatus === "generating") {
455
- setInitStatus("idle");
456
- setInitNeedOffer(false);
457
- setInitStep(0);
458
- setInitAnswers({});
459
- setGeneratedBio("");
460
- }
461
- return;
462
- }
463
-
464
  if (!isLoggedIn) return;
 
465
  if (!currentUserId) return;
466
 
467
- // already decided this session
468
  if (initStatus !== "idle") return;
469
 
470
  let cancelled = false;
471
 
472
  (async () => {
473
  try {
474
- const r = await fetch(
475
- `/api/profile/status?user_id=${encodeURIComponent(currentUserId)}`
476
- );
477
  if (!r.ok) return;
478
  const j = await r.json();
479
  if (cancelled) return;
@@ -496,22 +478,12 @@ export function ChatArea({
496
  e.preventDefault();
497
  if (!isLoggedIn) return;
498
 
499
- // INIT FLOW: treat input as the answer to the current init question
500
  if (chatMode === "ask" && initStatus === "asking") {
501
  const text = input.trim();
502
  if (!text) return;
503
 
504
  const q = INIT_QUESTIONS[initStep];
505
- if (!q) {
506
- // safety: out of range => reset
507
- setInitStatus("idle");
508
- setInitNeedOffer(false);
509
- setInitStep(0);
510
- setInitAnswers({});
511
- setGeneratedBio("");
512
- return;
513
- }
514
-
515
  const nextAnswers = { ...initAnswers, [q.id]: text };
516
 
517
  setInitAnswers(nextAnswers);
@@ -519,19 +491,10 @@ export function ChatArea({
519
 
520
  const nextStep = initStep + 1;
521
 
522
- // Ensure next bubble visible
523
- setTimeout(() => scrollToBottom("smooth"), 0);
524
-
525
  // finished -> generate bio + save to backend
526
  if (nextStep >= INIT_QUESTIONS.length) {
527
  setInitStatus("generating");
528
 
529
- if (!currentUserId) {
530
- toast.error("Missing user id. Please re-login and try again.");
531
- setInitStatus("asking");
532
- return;
533
- }
534
-
535
  try {
536
  const r = await fetch("/api/profile/init_submit", {
537
  method: "POST",
@@ -550,11 +513,9 @@ export function ChatArea({
550
  setInitStatus("done");
551
  setInitNeedOffer(false);
552
 
553
- // reset for future sessions if needed
554
  setInitStep(0);
555
  setInitAnswers({});
556
-
557
- setTimeout(() => scrollToBottom("smooth"), 0);
558
  } catch {
559
  toast.error("Sorry — I couldn’t generate your Bio. Please try again.");
560
  setInitStatus("asking");
@@ -568,9 +529,7 @@ export function ChatArea({
568
  return;
569
  }
570
 
571
- // ---------------------------
572
  // ORIGINAL behavior (unchanged)
573
- // ---------------------------
574
  const hasText = !!input.trim();
575
  const hasFiles = uploadedFiles.length > 0;
576
 
@@ -800,7 +759,7 @@ export function ChatArea({
800
  e.preventDefault();
801
  e.stopPropagation();
802
  if (!isLoggedIn) return;
803
- if (initLocked) return;
804
  setIsDragging(true);
805
  };
806
 
@@ -812,15 +771,12 @@ export function ChatArea({
812
 
813
  const MAX_UPLOAD_FILES = 10;
814
 
815
- // --------------------
816
- // handleDrop
817
- // --------------------
818
  const handleDrop = (e: React.DragEvent) => {
819
  e.preventDefault();
820
  e.stopPropagation();
821
  setIsDragging(false);
822
  if (!isLoggedIn) return;
823
- if (initLocked) return;
824
 
825
  const fileList = e.dataTransfer.files;
826
  const files: File[] = [];
@@ -850,7 +806,7 @@ export function ChatArea({
850
  return;
851
  }
852
 
853
- // limit total files per conversation
854
  const currentCount = uploadedFiles.length + pendingFiles.length;
855
  const remaining = MAX_UPLOAD_FILES - currentCount;
856
 
@@ -863,12 +819,10 @@ export function ChatArea({
863
  const rejected = validFiles.length - accepted.length;
864
 
865
  if (rejected > 0) {
866
- toast.warning(
867
- `Only the first ${accepted.length} file(s) were added (max ${MAX_UPLOAD_FILES}).`
868
- );
869
  }
870
 
871
- // append, do NOT overwrite existing pending
872
  setPendingFiles((prev) => [
873
  ...prev,
874
  ...accepted.map((file) => ({ file, type: "other" as FileType })),
@@ -877,10 +831,12 @@ export function ChatArea({
877
  setShowTypeDialog(true);
878
  };
879
 
880
- // --------------------
881
- // handleFileSelect
882
- // --------------------
883
  const handleFileSelect = (e: React.ChangeEvent<HTMLInputElement>) => {
 
 
 
 
 
884
  const files = Array.from(e.target.files || []) as File[];
885
 
886
  if (files.length === 0) {
@@ -924,9 +880,7 @@ export function ChatArea({
924
  const rejected = validFiles.length - accepted.length;
925
 
926
  if (rejected > 0) {
927
- toast.warning(
928
- `Only the first ${accepted.length} file(s) were added (max ${MAX_UPLOAD_FILES}).`
929
- );
930
  }
931
 
932
  // append, do NOT overwrite existing pending
@@ -961,9 +915,7 @@ export function ChatArea({
961
  };
962
 
963
  const handlePendingFileTypeChange = (index: number, type: FileType) => {
964
- setPendingFiles((prev) =>
965
- prev.map((pf, i) => (i === index ? { ...pf, type } : pf))
966
- );
967
  };
968
 
969
  // File helpers
@@ -972,8 +924,7 @@ export function ChatArea({
972
  if (ext.endsWith(".pdf")) return FileText;
973
  if (ext.endsWith(".docx") || ext.endsWith(".doc")) return File;
974
  if (ext.endsWith(".pptx") || ext.endsWith(".ppt")) return Presentation;
975
- if ([".jpg", ".jpeg", ".png", ".gif", ".webp"].some((e) => ext.endsWith(e)))
976
- return ImageIcon;
977
  return File;
978
  };
979
 
@@ -992,7 +943,7 @@ export function ChatArea({
992
 
993
  const { getOrCreate } = useObjectUrlCache(allThumbFiles);
994
 
995
- // NEW: a compact "chip" UI
996
  const FileChip = ({
997
  file,
998
  index,
@@ -1003,9 +954,7 @@ export function ChatArea({
1003
  source: "uploaded" | "pending";
1004
  }) => {
1005
  const ext = file.name.toLowerCase();
1006
- const isImage = [".jpg", ".jpeg", ".png", ".gif", ".webp"].some((e) =>
1007
- ext.endsWith(e)
1008
- );
1009
 
1010
  const isPdf = ext.endsWith(".pdf");
1011
  const isPpt = ext.endsWith(".ppt") || ext.endsWith(".pptx");
@@ -1050,6 +999,7 @@ export function ChatArea({
1050
  <div className="text-xs text-muted-foreground">{label}</div>
1051
  </div>
1052
 
 
1053
  <div className="relative h-10 w-10 flex-shrink-0 rounded-lg overflow-hidden border border-border bg-muted">
1054
  {isImage ? (
1055
  thumbUrl ? (
@@ -1079,6 +1029,11 @@ export function ChatArea({
1079
 
1080
  const bottomPad = Math.max(24, composerHeight + 24);
1081
 
 
 
 
 
 
1082
  return (
1083
  <div className="relative flex flex-col h-full min-h-0 w-full overflow-hidden">
1084
  {/* Top Bar */}
@@ -1166,24 +1121,18 @@ export function ChatArea({
1166
  variant="ghost"
1167
  size="icon"
1168
  onClick={handleSaveClick}
1169
- disabled={!isLoggedIn || initLocked}
1170
- className={`h-8 w-8 rounded-md hover:bg-muted/50 ${
1171
- isCurrentChatSaved() ? "text-primary" : ""
1172
- }`}
1173
  title={isCurrentChatSaved() ? "Unsave" : "Save"}
1174
  >
1175
- <Bookmark
1176
- className={`h-4 w-4 ${
1177
- isCurrentChatSaved() ? "fill-primary text-primary" : ""
1178
- }`}
1179
- />
1180
  </Button>
1181
 
1182
  <Button
1183
  variant="ghost"
1184
  size="icon"
1185
  onClick={handleOpenDownloadDialog}
1186
- disabled={!isLoggedIn || initLocked}
1187
  className="h-8 w-8 rounded-md hover:bg-muted/50"
1188
  title="Download"
1189
  >
@@ -1194,7 +1143,7 @@ export function ChatArea({
1194
  variant="ghost"
1195
  size="icon"
1196
  onClick={handleShareClick}
1197
- disabled={!isLoggedIn || initLocked}
1198
  className="h-8 w-8 rounded-md hover:bg-muted/50"
1199
  title="Share"
1200
  >
@@ -1204,7 +1153,7 @@ export function ChatArea({
1204
  <Button
1205
  variant="outline"
1206
  onClick={handleClearClick}
1207
- disabled={!isLoggedIn || initLocked}
1208
  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)]"
1209
  title="New Chat"
1210
  >
@@ -1228,9 +1177,7 @@ export function ChatArea({
1228
  message={message}
1229
  showSenderInfo={spaceType === "group"}
1230
  isFirstGreeting={
1231
- (message.id === "1" ||
1232
- message.id === "review-1" ||
1233
- message.id === "quiz-1") &&
1234
  message.role === "assistant"
1235
  }
1236
  showNextButton={message.showNextButton && !isAppTyping}
@@ -1241,22 +1188,14 @@ export function ChatArea({
1241
  docType={docType}
1242
  />
1243
 
1244
- {chatMode === "review" &&
1245
- message.id === "review-1" &&
1246
- message.role === "assistant" && (
1247
- <div className="flex gap-2 justify-start px-4">
1248
- <div className="w-10 h-10 flex-shrink-0" />
1249
- <div
1250
- className="w-full"
1251
- style={{ maxWidth: "min(770px, calc(100% - 2rem))" }}
1252
- >
1253
- <SmartReview
1254
- onReviewTopic={handleReviewTopic}
1255
- onReviewAll={handleReviewAll}
1256
- />
1257
- </div>
1258
  </div>
1259
- )}
 
1260
 
1261
  {chatMode === "quiz" &&
1262
  message.id === "quiz-1" &&
@@ -1265,10 +1204,7 @@ export function ChatArea({
1265
  !quizState.waitingForAnswer &&
1266
  !isAppTyping && (
1267
  <div className="flex justify-center py-4">
1268
- <Button
1269
- onClick={onStartQuiz}
1270
- className="bg-red-500 hover:bg-red-600 text-white"
1271
- >
1272
  Start Quiz
1273
  </Button>
1274
  </div>
@@ -1276,38 +1212,34 @@ export function ChatArea({
1276
  </React.Fragment>
1277
  ))}
1278
 
1279
- {/* Profile Init Offer (Ask mode only) */}
1280
  {chatMode === "ask" && initNeedOffer && initStatus === "offered" && (
1281
  <div className="flex gap-2 justify-start px-4">
1282
  <div className="w-10 h-10 rounded-full overflow-hidden bg-white flex items-center justify-center flex-shrink-0">
1283
- <img
1284
- src={clareAvatar}
1285
- alt="Clare"
1286
- className="w-full h-full object-cover"
1287
- />
1288
  </div>
1289
 
1290
  <div className="w-full" style={{ maxWidth: "min(770px, calc(100% - 2rem))" }}>
1291
  <div className="rounded-2xl border bg-card px-4 py-3 space-y-3">
1292
- <div className="font-semibold">
1293
- Quick intro so I can personalize your experience
1294
- </div>
1295
  <div className="text-sm text-muted-foreground leading-relaxed">
1296
- I’m Clare, your AI teaching assistant. If you’d like, we can
1297
- answer a few short questions so I can tailor explanations,
1298
- pacing, and practice to you. Your answers will be summarized
1299
- into your Profile Bio and used only inside this platform.
1300
  </div>
1301
  <div className="flex gap-2">
1302
  <Button
1303
  type="button"
1304
  onClick={() => {
1305
- setInitNeedOffer(false);
1306
  setInitStatus("asking");
1307
  setInitStep(0);
1308
  setInitAnswers({});
1309
  setGeneratedBio("");
1310
- setTimeout(() => scrollToBottom("smooth"), 0);
 
 
 
 
1311
  }}
1312
  >
1313
  Yes — let’s start
@@ -1317,13 +1249,11 @@ export function ChatArea({
1317
  variant="outline"
1318
  onClick={async () => {
1319
  try {
1320
- if (currentUserId) {
1321
- await fetch("/api/profile/dismiss", {
1322
- method: "POST",
1323
- headers: { "Content-Type": "application/json" },
1324
- body: JSON.stringify({ user_id: currentUserId, days: 7 }),
1325
- });
1326
- }
1327
  } catch {}
1328
  setInitNeedOffer(false);
1329
  setInitStatus("idle");
@@ -1337,15 +1267,11 @@ export function ChatArea({
1337
  </div>
1338
  )}
1339
 
1340
- {/* Current init question bubble */}
1341
  {chatMode === "ask" && initStatus === "asking" && (
1342
  <div className="flex gap-2 justify-start px-4">
1343
  <div className="w-10 h-10 rounded-full overflow-hidden bg-white flex items-center justify-center flex-shrink-0">
1344
- <img
1345
- src={clareAvatar}
1346
- alt="Clare"
1347
- className="w-full h-full object-cover"
1348
- />
1349
  </div>
1350
 
1351
  <div
@@ -1356,8 +1282,7 @@ export function ChatArea({
1356
  {INIT_QUESTIONS[initStep]?.title}
1357
  </div>
1358
  <div className="text-xs text-muted-foreground">
1359
- {INIT_QUESTIONS[initStep]?.placeholder ||
1360
- "Just type your answer and press Send."}
1361
  </div>
1362
  <div className="text-xs text-muted-foreground mt-2">
1363
  Question {initStep + 1} of {INIT_QUESTIONS.length}
@@ -1366,33 +1291,23 @@ export function ChatArea({
1366
  </div>
1367
  )}
1368
 
1369
- {/* Generating bubble */}
1370
  {chatMode === "ask" && initStatus === "generating" && (
1371
  <div className="flex gap-2 justify-start px-4">
1372
  <div className="w-10 h-10 rounded-full overflow-hidden bg-white flex items-center justify-center flex-shrink-0">
1373
- <img
1374
- src={clareAvatar}
1375
- alt="Clare"
1376
- className="w-full h-full object-cover"
1377
- />
1378
  </div>
1379
  <div className="bg-muted rounded-2xl px-4 py-3">
1380
- <div className="text-sm">
1381
- Thanks — I’m generating your Profile Bio now…
1382
- </div>
1383
  </div>
1384
  </div>
1385
  )}
1386
 
1387
- {/* Done bubble with bio */}
1388
  {chatMode === "ask" && initStatus === "done" && !!generatedBio && (
1389
  <div className="flex gap-2 justify-start px-4">
1390
  <div className="w-10 h-10 rounded-full overflow-hidden bg-white flex items-center justify-center flex-shrink-0">
1391
- <img
1392
- src={clareAvatar}
1393
- alt="Clare"
1394
- className="w-full h-full object-cover"
1395
- />
1396
  </div>
1397
 
1398
  <div className="w-full" style={{ maxWidth: "min(770px, calc(100% - 2rem))" }}>
@@ -1402,8 +1317,7 @@ export function ChatArea({
1402
  </div>
1403
  <div className="text-sm whitespace-pre-wrap">{generatedBio}</div>
1404
  <div className="text-xs text-muted-foreground">
1405
- You can update it anytime. I’ll use this to adapt explanations,
1406
- pacing, and practice.
1407
  </div>
1408
  </div>
1409
  </div>
@@ -1413,11 +1327,7 @@ export function ChatArea({
1413
  {isAppTyping && (
1414
  <div className="flex gap-2 justify-start px-4">
1415
  <div className="w-10 h-10 rounded-full overflow-hidden bg-white flex items-center justify-center flex-shrink-0">
1416
- <img
1417
- src={clareAvatar}
1418
- alt="Clare"
1419
- className="w-full h-full object-cover"
1420
- />
1421
  </div>
1422
  <div className="bg-muted rounded-2xl px-4 py-3">
1423
  <div className="flex gap-1">
@@ -1460,10 +1370,7 @@ export function ChatArea({
1460
  )}
1461
 
1462
  {/* Composer */}
1463
- <div
1464
- ref={composerRef}
1465
- className="flex-shrink-0 bg-background/95 backdrop-blur-sm z-20 border-t border-border"
1466
- >
1467
  <div className="max-w-4xl mx-auto px-4 py-4">
1468
  {/* Uploaded Files Preview */}
1469
  {(uploadedFiles.length > 0 || pendingFiles.length > 0) && (
@@ -1502,6 +1409,7 @@ export function ChatArea({
1502
  className="flex items-center justify-between gap-2 rounded-md border px-3 py-2 cursor-pointer hover:bg-muted/40"
1503
  title="Click to preview"
1504
  >
 
1505
  <div className="h-10 w-10 flex-shrink-0 rounded-lg overflow-hidden border border-border bg-muted">
1506
  {isImage ? (
1507
  thumbUrl ? (
@@ -1536,11 +1444,11 @@ export function ChatArea({
1536
  size="icon"
1537
  onClick={(e) => {
1538
  e.preventDefault();
1539
- e.stopPropagation();
1540
  onRemoveFile(i);
1541
  }}
1542
  title="Remove"
1543
- disabled={initLocked}
1544
  >
1545
  <Trash2 className="h-4 w-4" />
1546
  </Button>
@@ -1577,7 +1485,7 @@ export function ChatArea({
1577
  variant="ghost"
1578
  size="sm"
1579
  className="gap-1.5 h-8 px-2 text-xs hover:bg-muted/50"
1580
- disabled={!isLoggedIn || initLocked}
1581
  type="button"
1582
  >
1583
  <span>{modeLabels[learningMode]}</span>
@@ -1678,7 +1586,7 @@ export function ChatArea({
1678
  variant="ghost"
1679
  disabled={
1680
  !isLoggedIn ||
1681
- initLocked ||
1682
  (chatMode === "quiz" && !quizState.waitingForAnswer)
1683
  }
1684
  className="h-8 w-8 hover:bg-muted/50"
@@ -1694,7 +1602,8 @@ export function ChatArea({
1694
  onChange={(e) => setInput(e.target.value)}
1695
  onKeyDown={handleKeyDown}
1696
  placeholder={
1697
- !isLoggedIn
 
1698
  ? "Please log in on the right to start chatting..."
1699
  : chatMode === "quiz"
1700
  ? quizState.waitingForAnswer
@@ -1706,11 +1615,11 @@ export function ChatArea({
1706
  ? "Type a message or drag files here... (mention @Clare to get AI assistance)"
1707
  : learningMode === "general"
1708
  ? "Ask me anything! Please provide context about your question..."
1709
- : "Ask Clare anything about the course or drag files here..."
1710
  }
1711
  disabled={
1712
  !isLoggedIn ||
1713
- initLocked ||
1714
  (chatMode === "quiz" && !quizState.waitingForAnswer)
1715
  }
1716
  className={`min-h-[80px] pl-4 pr-20 resize-none bg-background border-2 ${
@@ -1723,10 +1632,9 @@ export function ChatArea({
1723
  type="submit"
1724
  size="icon"
1725
  disabled={
 
1726
  !isLoggedIn ||
1727
- (chatMode === "ask" && initStatus === "asking"
1728
- ? !input.trim()
1729
- : !input.trim() && uploadedFiles.length === 0)
1730
  }
1731
  className="h-8 w-8 rounded-full"
1732
  >
@@ -1741,14 +1649,14 @@ export function ChatArea({
1741
  accept=".pdf,.docx,.pptx,.doc,.ppt,.jpg,.jpeg,.png,.gif,.webp"
1742
  onChange={handleFileSelect}
1743
  className="hidden"
1744
- disabled={!isLoggedIn || initLocked}
1745
  />
1746
  </div>
1747
  </form>
1748
  </div>
1749
  </div>
1750
 
1751
- {/* ✅ NEW: File Viewer Dialog */}
1752
  <Dialog
1753
  open={showFileViewer}
1754
  onOpenChange={(open) => {
@@ -1801,17 +1709,10 @@ export function ChatArea({
1801
  </AlertDialogHeader>
1802
 
1803
  <AlertDialogFooter className="flex-col sm:flex-row gap-2 sm:justify-end">
1804
- <Button
1805
- variant="outline"
1806
- onClick={() => onConfirmClear(false)}
1807
- className="sm:flex-1 sm:max-w-[200px]"
1808
- >
1809
  Start New (Don't Save)
1810
  </Button>
1811
- <AlertDialogAction
1812
- onClick={() => onConfirmClear(true)}
1813
- className="sm:flex-1 sm:max-w-[200px]"
1814
- >
1815
  Save & Start New
1816
  </AlertDialogAction>
1817
  </AlertDialogFooter>
@@ -1867,9 +1768,7 @@ export function ChatArea({
1867
  <Checkbox
1868
  id="download-chat"
1869
  checked={downloadOptions.chat}
1870
- onCheckedChange={(checked) =>
1871
- setDownloadOptions({ ...downloadOptions, chat: checked === true })
1872
- }
1873
  />
1874
  <label htmlFor="download-chat" className="text-sm font-medium cursor-pointer">
1875
  Download chat
@@ -1879,9 +1778,7 @@ export function ChatArea({
1879
  <Checkbox
1880
  id="download-summary"
1881
  checked={downloadOptions.summary}
1882
- onCheckedChange={(checked) =>
1883
- setDownloadOptions({ ...downloadOptions, summary: checked === true })
1884
- }
1885
  />
1886
  <label htmlFor="download-summary" className="text-sm font-medium cursor-pointer">
1887
  Download summary
@@ -1970,40 +1867,13 @@ export function ChatArea({
1970
  </AlertDialogContent>
1971
  </AlertDialog>
1972
 
1973
- {/* File Viewer Dialog (kept as in your original; redundant but unchanged) */}
1974
- <Dialog open={showFileViewer} onOpenChange={setShowFileViewer}>
1975
- <DialogContent className="max-w-4xl max-h-[85vh] flex flex-col overflow-hidden">
1976
- <DialogHeader className="min-w-0 flex-shrink-0">
1977
- <DialogTitle
1978
- className="pr-8 break-words break-all overflow-wrap-anywhere leading-relaxed"
1979
- style={{
1980
- wordBreak: "break-all",
1981
- overflowWrap: "anywhere",
1982
- maxWidth: "100%",
1983
- lineHeight: "1.6",
1984
- }}
1985
- >
1986
- {selectedFile?.file.name}
1987
- </DialogTitle>
1988
- <DialogDescription>
1989
- File size: {selectedFile ? formatFileSize(selectedFile.file.size) : ""}
1990
- </DialogDescription>
1991
- </DialogHeader>
1992
- <div className="flex-1 min-h-0 overflow-y-auto mt-4">
1993
- {selectedFile && <FileViewerContent file={selectedFile.file} />}
1994
- </div>
1995
- </DialogContent>
1996
- </Dialog>
1997
-
1998
  {/* File Type Selection Dialog */}
1999
  {showTypeDialog && (
2000
  <Dialog open={showTypeDialog} onOpenChange={setShowTypeDialog}>
2001
  <DialogContent className="sm:max-w-[425px]" style={{ zIndex: 99999 }}>
2002
  <DialogHeader>
2003
  <DialogTitle>Select File Types</DialogTitle>
2004
- <DialogDescription>
2005
- Please select the type for each file you are uploading.
2006
- </DialogDescription>
2007
  </DialogHeader>
2008
 
2009
  <div className="space-y-3 max-h-64 overflow-y-auto">
@@ -2015,9 +1885,7 @@ export function ChatArea({
2015
  <Icon className="h-4 w-4 text-muted-foreground flex-shrink-0" />
2016
  <div className="flex-1 min-w-0">
2017
  <p className="text-sm truncate">{pendingFile.file.name}</p>
2018
- <p className="text-xs text-muted-foreground">
2019
- {formatFileSize(pendingFile.file.size)}
2020
- </p>
2021
  </div>
2022
  </div>
2023
 
@@ -2025,24 +1893,15 @@ export function ChatArea({
2025
  <label className="text-xs text-muted-foreground">File Type</label>
2026
  <Select
2027
  value={pendingFile.type}
2028
- onValueChange={(value) =>
2029
- handlePendingFileTypeChange(index, value as FileType)
2030
- }
2031
  >
2032
  <SelectTrigger className="h-8 text-xs">
2033
  <SelectValue />
2034
  </SelectTrigger>
2035
- <SelectContent
2036
- className="!z-[100000] !bg-background !text-foreground"
2037
- style={{ zIndex: 100000 }}
2038
- >
2039
  <SelectItem value="syllabus">Syllabus</SelectItem>
2040
- <SelectItem value="lecture-slides">
2041
- Lecture Slides / PPT
2042
- </SelectItem>
2043
- <SelectItem value="literature-review">
2044
- Literature Review / Paper
2045
- </SelectItem>
2046
  <SelectItem value="other">Other Course Document</SelectItem>
2047
  </SelectContent>
2048
  </Select>
 
133
  type: FileType;
134
  }
135
 
136
+ // 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));
 
173
  }
174
 
175
  if (isPdfFile(file.name)) {
176
+ // Force PDF MIME, in case file.type is empty and the browser blocks preview.
177
  const pdfBlob = new Blob([file], { type: "application/pdf" });
178
  const pdfUrl = URL.createObjectURL(pdfBlob);
179
 
 
182
  <object data={pdfUrl} type="application/pdf" className="w-full h-full">
183
  <div className="p-3 space-y-2">
184
  <div className="text-sm text-muted-foreground">
185
+ PDF preview is blocked by your browser. Please open it in a new tab or download.
 
186
  </div>
187
  <div className="flex gap-2">
188
  <a
 
216
  return (
217
  <div className="space-y-3">
218
  <div className="text-sm text-muted-foreground">
219
+ Preview is not available for this {kind} format in the browser without conversion.
 
220
  </div>
221
  <a
222
  href={url}
 
229
  );
230
  }
231
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
232
  export function ChatArea({
233
  messages,
234
  onSendMessage,
 
295
  const [targetWorkspaceId, setTargetWorkspaceId] = useState<string>("");
296
 
297
  // --------------------------
298
+ // Profile Init Flow (Ask mode)
299
  // --------------------------
300
+ type InitStatus = "idle" | "offered" | "asking" | "generating" | "done";
301
+
302
+ type InitQ = {
303
+ id: string;
304
+ title: string;
305
+ placeholder?: string;
306
+ };
307
+
308
+ const INIT_QUESTIONS: InitQ[] = [
309
+ {
310
+ id: "course_goal",
311
+ title: "What’s the single most important outcome you want from this course?",
312
+ placeholder: "e.g., understand LLM basics, build a project, prep for an exam, apply to work…",
313
+ },
314
+ {
315
+ id: "background",
316
+ title: "What’s your current background (major, job, or anything relevant)?",
317
+ placeholder: "One sentence is totally fine.",
318
+ },
319
+ {
320
+ id: "ai_experience",
321
+ title: "Have you worked with AI/LLMs before? If yes, at what level?",
322
+ placeholder: "e.g., none / used ChatGPT / built small projects / research…",
323
+ },
324
+ {
325
+ id: "python_level",
326
+ title: "How comfortable are you with Python? (Beginner / Intermediate / Advanced)",
327
+ placeholder: "Type one: Beginner / Intermediate / Advanced",
328
+ },
329
+ {
330
+ id: "preferred_format",
331
+ title: "What helps you learn best? (You can list multiple, separated by commas)",
332
+ placeholder: "Step-by-step, examples, visuals, concise answers, Socratic questions…",
333
+ },
334
+ {
335
+ id: "pace",
336
+ title: "What pace do you prefer from me? (Fast / Steady / Very detailed)",
337
+ placeholder: "Type one: Fast / Steady / Very detailed",
338
+ },
339
+ {
340
+ id: "biggest_pain",
341
+ title: "Where do you typically get stuck when learning technical topics?",
342
+ placeholder: "Concepts, tools, task breakdown, math, confidence, time management…",
343
+ },
344
+ {
345
+ id: "support_pref",
346
+ title: "When you’re unsure, how should I support you?",
347
+ placeholder: "Hints first / guided questions / direct answer / ask then answer…",
348
+ },
349
+ ];
350
+
351
  const [initStatus, setInitStatus] = useState<InitStatus>("idle");
352
  const [initNeedOffer, setInitNeedOffer] = useState(false);
353
  const [initStep, setInitStep] = useState(0);
354
  const [initAnswers, setInitAnswers] = useState<Record<string, any>>({});
355
  const [generatedBio, setGeneratedBio] = useState<string>("");
356
 
357
+ // IMPORTANT: allow typing during "asking"; lock typing only during "generating"
358
+ const initInputLocked = chatMode === "ask" && initStatus === "generating";
359
+ // Use this to block other actions (uploads / drag-drop / etc.) during asking+generating
360
+ const initBlockActions = chatMode === "ask" && (initStatus === "asking" || initStatus === "generating");
361
 
362
  const courses =
363
  availableCourses.length > 0
 
373
  const scrollContainerRef = useRef<HTMLDivElement>(null);
374
  const fileInputRef = useRef<HTMLInputElement>(null);
375
 
376
+ // Composer measured height (dynamic) to reserve bottom padding for messages
377
  const composerRef = useRef<HTMLDivElement>(null);
378
  const [composerHeight, setComposerHeight] = useState<number>(160);
379
 
 
419
  if (messages.length > previousMessagesLength.current) {
420
  const el = scrollContainerRef.current;
421
  if (el) {
422
+ const nearBottom = el.scrollHeight - el.scrollTop - el.clientHeight < 240;
 
423
  if (nearBottom) scrollToBottom("smooth");
424
  }
425
  }
 
442
  return () => container.removeEventListener("scroll", handleScroll);
443
  }, []);
444
 
445
+ // Check if we should run profile init flow (Ask mode only)
446
  useEffect(() => {
 
 
 
 
 
 
 
 
 
 
 
 
447
  if (!isLoggedIn) return;
448
+ if (chatMode !== "ask") return;
449
  if (!currentUserId) return;
450
 
451
+ // If already completed in this session, do nothing
452
  if (initStatus !== "idle") return;
453
 
454
  let cancelled = false;
455
 
456
  (async () => {
457
  try {
458
+ const r = await fetch(`/api/profile/status?user_id=${encodeURIComponent(currentUserId)}`);
 
 
459
  if (!r.ok) return;
460
  const j = await r.json();
461
  if (cancelled) return;
 
478
  e.preventDefault();
479
  if (!isLoggedIn) return;
480
 
481
+ // INIT FLOW: treat input as the answer to the current init question
482
  if (chatMode === "ask" && initStatus === "asking") {
483
  const text = input.trim();
484
  if (!text) return;
485
 
486
  const q = INIT_QUESTIONS[initStep];
 
 
 
 
 
 
 
 
 
 
487
  const nextAnswers = { ...initAnswers, [q.id]: text };
488
 
489
  setInitAnswers(nextAnswers);
 
491
 
492
  const nextStep = initStep + 1;
493
 
 
 
 
494
  // finished -> generate bio + save to backend
495
  if (nextStep >= INIT_QUESTIONS.length) {
496
  setInitStatus("generating");
497
 
 
 
 
 
 
 
498
  try {
499
  const r = await fetch("/api/profile/init_submit", {
500
  method: "POST",
 
513
  setInitStatus("done");
514
  setInitNeedOffer(false);
515
 
516
+ // reset
517
  setInitStep(0);
518
  setInitAnswers({});
 
 
519
  } catch {
520
  toast.error("Sorry — I couldn’t generate your Bio. Please try again.");
521
  setInitStatus("asking");
 
529
  return;
530
  }
531
 
 
532
  // ORIGINAL behavior (unchanged)
 
533
  const hasText = !!input.trim();
534
  const hasFiles = uploadedFiles.length > 0;
535
 
 
759
  e.preventDefault();
760
  e.stopPropagation();
761
  if (!isLoggedIn) return;
762
+ if (initBlockActions) return; // block drag UI during init
763
  setIsDragging(true);
764
  };
765
 
 
771
 
772
  const MAX_UPLOAD_FILES = 10;
773
 
 
 
 
774
  const handleDrop = (e: React.DragEvent) => {
775
  e.preventDefault();
776
  e.stopPropagation();
777
  setIsDragging(false);
778
  if (!isLoggedIn) return;
779
+ if (initBlockActions) return; // block file drop during init
780
 
781
  const fileList = e.dataTransfer.files;
782
  const files: File[] = [];
 
806
  return;
807
  }
808
 
809
+ // limit total files per conversation
810
  const currentCount = uploadedFiles.length + pendingFiles.length;
811
  const remaining = MAX_UPLOAD_FILES - currentCount;
812
 
 
819
  const rejected = validFiles.length - accepted.length;
820
 
821
  if (rejected > 0) {
822
+ toast.warning(`Only the first ${accepted.length} file(s) were added (max ${MAX_UPLOAD_FILES}).`);
 
 
823
  }
824
 
825
+ // append, do NOT overwrite existing pending
826
  setPendingFiles((prev) => [
827
  ...prev,
828
  ...accepted.map((file) => ({ file, type: "other" as FileType })),
 
831
  setShowTypeDialog(true);
832
  };
833
 
 
 
 
834
  const handleFileSelect = (e: React.ChangeEvent<HTMLInputElement>) => {
835
+ if (initBlockActions) {
836
+ e.target.value = "";
837
+ return;
838
+ }
839
+
840
  const files = Array.from(e.target.files || []) as File[];
841
 
842
  if (files.length === 0) {
 
880
  const rejected = validFiles.length - accepted.length;
881
 
882
  if (rejected > 0) {
883
+ toast.warning(`Only the first ${accepted.length} file(s) were added (max ${MAX_UPLOAD_FILES}).`);
 
 
884
  }
885
 
886
  // append, do NOT overwrite existing pending
 
915
  };
916
 
917
  const handlePendingFileTypeChange = (index: number, type: FileType) => {
918
+ setPendingFiles((prev) => prev.map((pf, i) => (i === index ? { ...pf, type } : pf)));
 
 
919
  };
920
 
921
  // File helpers
 
924
  if (ext.endsWith(".pdf")) return FileText;
925
  if (ext.endsWith(".docx") || ext.endsWith(".doc")) return File;
926
  if (ext.endsWith(".pptx") || ext.endsWith(".ppt")) return Presentation;
927
+ if ([".jpg", ".jpeg", ".png", ".gif", ".webp"].some((e) => ext.endsWith(e))) return ImageIcon;
 
928
  return File;
929
  };
930
 
 
943
 
944
  const { getOrCreate } = useObjectUrlCache(allThumbFiles);
945
 
946
+ // a compact "chip" UI (the one with left Trash)
947
  const FileChip = ({
948
  file,
949
  index,
 
954
  source: "uploaded" | "pending";
955
  }) => {
956
  const ext = file.name.toLowerCase();
957
+ const isImage = [".jpg", ".jpeg", ".png", ".gif", ".webp"].some((e) => ext.endsWith(e));
 
 
958
 
959
  const isPdf = ext.endsWith(".pdf");
960
  const isPpt = ext.endsWith(".ppt") || ext.endsWith(".pptx");
 
999
  <div className="text-xs text-muted-foreground">{label}</div>
1000
  </div>
1001
 
1002
+ {/* Thumbnail (image preview or file icon) */}
1003
  <div className="relative h-10 w-10 flex-shrink-0 rounded-lg overflow-hidden border border-border bg-muted">
1004
  {isImage ? (
1005
  thumbUrl ? (
 
1029
 
1030
  const bottomPad = Math.max(24, composerHeight + 24);
1031
 
1032
+ const initPlaceholder =
1033
+ initStatus === "asking"
1034
+ ? "Type your answer here and press Enter / Send..."
1035
+ : undefined;
1036
+
1037
  return (
1038
  <div className="relative flex flex-col h-full min-h-0 w-full overflow-hidden">
1039
  {/* Top Bar */}
 
1121
  variant="ghost"
1122
  size="icon"
1123
  onClick={handleSaveClick}
1124
+ disabled={!isLoggedIn || initBlockActions}
1125
+ className={`h-8 w-8 rounded-md hover:bg-muted/50 ${isCurrentChatSaved() ? "text-primary" : ""}`}
 
 
1126
  title={isCurrentChatSaved() ? "Unsave" : "Save"}
1127
  >
1128
+ <Bookmark className={`h-4 w-4 ${isCurrentChatSaved() ? "fill-primary text-primary" : ""}`} />
 
 
 
 
1129
  </Button>
1130
 
1131
  <Button
1132
  variant="ghost"
1133
  size="icon"
1134
  onClick={handleOpenDownloadDialog}
1135
+ disabled={!isLoggedIn || initBlockActions}
1136
  className="h-8 w-8 rounded-md hover:bg-muted/50"
1137
  title="Download"
1138
  >
 
1143
  variant="ghost"
1144
  size="icon"
1145
  onClick={handleShareClick}
1146
+ disabled={!isLoggedIn || initBlockActions}
1147
  className="h-8 w-8 rounded-md hover:bg-muted/50"
1148
  title="Share"
1149
  >
 
1153
  <Button
1154
  variant="outline"
1155
  onClick={handleClearClick}
1156
+ disabled={!isLoggedIn || initBlockActions}
1157
  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)]"
1158
  title="New Chat"
1159
  >
 
1177
  message={message}
1178
  showSenderInfo={spaceType === "group"}
1179
  isFirstGreeting={
1180
+ (message.id === "1" || message.id === "review-1" || message.id === "quiz-1") &&
 
 
1181
  message.role === "assistant"
1182
  }
1183
  showNextButton={message.showNextButton && !isAppTyping}
 
1188
  docType={docType}
1189
  />
1190
 
1191
+ {chatMode === "review" && message.id === "review-1" && message.role === "assistant" && (
1192
+ <div className="flex gap-2 justify-start px-4">
1193
+ <div className="w-10 h-10 flex-shrink-0" />
1194
+ <div className="w-full" style={{ maxWidth: "min(770px, calc(100% - 2rem))" }}>
1195
+ <SmartReview onReviewTopic={handleReviewTopic} onReviewAll={handleReviewAll} />
 
 
 
 
 
 
 
 
 
1196
  </div>
1197
+ </div>
1198
+ )}
1199
 
1200
  {chatMode === "quiz" &&
1201
  message.id === "quiz-1" &&
 
1204
  !quizState.waitingForAnswer &&
1205
  !isAppTyping && (
1206
  <div className="flex justify-center py-4">
1207
+ <Button onClick={onStartQuiz} className="bg-red-500 hover:bg-red-600 text-white">
 
 
 
1208
  Start Quiz
1209
  </Button>
1210
  </div>
 
1212
  </React.Fragment>
1213
  ))}
1214
 
1215
+ {/* Profile Init Offer (Ask mode only) */}
1216
  {chatMode === "ask" && initNeedOffer && initStatus === "offered" && (
1217
  <div className="flex gap-2 justify-start px-4">
1218
  <div className="w-10 h-10 rounded-full overflow-hidden bg-white flex items-center justify-center flex-shrink-0">
1219
+ <img src={clareAvatar} alt="Clare" className="w-full h-full object-cover" />
 
 
 
 
1220
  </div>
1221
 
1222
  <div className="w-full" style={{ maxWidth: "min(770px, calc(100% - 2rem))" }}>
1223
  <div className="rounded-2xl border bg-card px-4 py-3 space-y-3">
1224
+ <div className="font-semibold">Quick intro so I can personalize your experience</div>
 
 
1225
  <div className="text-sm text-muted-foreground leading-relaxed">
1226
+ I’m Clare, your AI teaching assistant. If you’d like, we can answer a few short questions so I can
1227
+ tailor explanations, pacing, and practice to you. Your answers will be summarized into your Profile Bio
1228
+ and used only inside this platform.
 
1229
  </div>
1230
  <div className="flex gap-2">
1231
  <Button
1232
  type="button"
1233
  onClick={() => {
 
1234
  setInitStatus("asking");
1235
  setInitStep(0);
1236
  setInitAnswers({});
1237
  setGeneratedBio("");
1238
+ // Keep focus in the composer for immediate typing
1239
+ setTimeout(() => {
1240
+ const ta = document.querySelector("textarea");
1241
+ (ta as HTMLTextAreaElement | null)?.focus?.();
1242
+ }, 0);
1243
  }}
1244
  >
1245
  Yes — let’s start
 
1249
  variant="outline"
1250
  onClick={async () => {
1251
  try {
1252
+ await fetch("/api/profile/dismiss", {
1253
+ method: "POST",
1254
+ headers: { "Content-Type": "application/json" },
1255
+ body: JSON.stringify({ user_id: currentUserId, days: 7 }),
1256
+ });
 
 
1257
  } catch {}
1258
  setInitNeedOffer(false);
1259
  setInitStatus("idle");
 
1267
  </div>
1268
  )}
1269
 
1270
+ {/* Current init question bubble */}
1271
  {chatMode === "ask" && initStatus === "asking" && (
1272
  <div className="flex gap-2 justify-start px-4">
1273
  <div className="w-10 h-10 rounded-full overflow-hidden bg-white flex items-center justify-center flex-shrink-0">
1274
+ <img src={clareAvatar} alt="Clare" className="w-full h-full object-cover" />
 
 
 
 
1275
  </div>
1276
 
1277
  <div
 
1282
  {INIT_QUESTIONS[initStep]?.title}
1283
  </div>
1284
  <div className="text-xs text-muted-foreground">
1285
+ {INIT_QUESTIONS[initStep]?.placeholder || "Just type your answer and press Send."}
 
1286
  </div>
1287
  <div className="text-xs text-muted-foreground mt-2">
1288
  Question {initStep + 1} of {INIT_QUESTIONS.length}
 
1291
  </div>
1292
  )}
1293
 
1294
+ {/* Generating bubble */}
1295
  {chatMode === "ask" && initStatus === "generating" && (
1296
  <div className="flex gap-2 justify-start px-4">
1297
  <div className="w-10 h-10 rounded-full overflow-hidden bg-white flex items-center justify-center flex-shrink-0">
1298
+ <img src={clareAvatar} alt="Clare" className="w-full h-full object-cover" />
 
 
 
 
1299
  </div>
1300
  <div className="bg-muted rounded-2xl px-4 py-3">
1301
+ <div className="text-sm">Thanks — I’m generating your Profile Bio now…</div>
 
 
1302
  </div>
1303
  </div>
1304
  )}
1305
 
1306
+ {/* Done bubble with bio */}
1307
  {chatMode === "ask" && initStatus === "done" && !!generatedBio && (
1308
  <div className="flex gap-2 justify-start px-4">
1309
  <div className="w-10 h-10 rounded-full overflow-hidden bg-white flex items-center justify-center flex-shrink-0">
1310
+ <img src={clareAvatar} alt="Clare" className="w-full h-full object-cover" />
 
 
 
 
1311
  </div>
1312
 
1313
  <div className="w-full" style={{ maxWidth: "min(770px, calc(100% - 2rem))" }}>
 
1317
  </div>
1318
  <div className="text-sm whitespace-pre-wrap">{generatedBio}</div>
1319
  <div className="text-xs text-muted-foreground">
1320
+ You can update it anytime. I’ll use this to adapt explanations, pacing, and practice.
 
1321
  </div>
1322
  </div>
1323
  </div>
 
1327
  {isAppTyping && (
1328
  <div className="flex gap-2 justify-start px-4">
1329
  <div className="w-10 h-10 rounded-full overflow-hidden bg-white flex items-center justify-center flex-shrink-0">
1330
+ <img src={clareAvatar} alt="Clare" className="w-full h-full object-cover" />
 
 
 
 
1331
  </div>
1332
  <div className="bg-muted rounded-2xl px-4 py-3">
1333
  <div className="flex gap-1">
 
1370
  )}
1371
 
1372
  {/* Composer */}
1373
+ <div ref={composerRef} className="flex-shrink-0 bg-background/95 backdrop-blur-sm z-20 border-t border-border">
 
 
 
1374
  <div className="max-w-4xl mx-auto px-4 py-4">
1375
  {/* Uploaded Files Preview */}
1376
  {(uploadedFiles.length > 0 || pendingFiles.length > 0) && (
 
1409
  className="flex items-center justify-between gap-2 rounded-md border px-3 py-2 cursor-pointer hover:bg-muted/40"
1410
  title="Click to preview"
1411
  >
1412
+ {/* Thumbnail (image preview or file icon) */}
1413
  <div className="h-10 w-10 flex-shrink-0 rounded-lg overflow-hidden border border-border bg-muted">
1414
  {isImage ? (
1415
  thumbUrl ? (
 
1444
  size="icon"
1445
  onClick={(e) => {
1446
  e.preventDefault();
1447
+ e.stopPropagation(); // don't open viewer
1448
  onRemoveFile(i);
1449
  }}
1450
  title="Remove"
1451
+ disabled={initBlockActions}
1452
  >
1453
  <Trash2 className="h-4 w-4" />
1454
  </Button>
 
1485
  variant="ghost"
1486
  size="sm"
1487
  className="gap-1.5 h-8 px-2 text-xs hover:bg-muted/50"
1488
+ disabled={!isLoggedIn || initBlockActions}
1489
  type="button"
1490
  >
1491
  <span>{modeLabels[learningMode]}</span>
 
1586
  variant="ghost"
1587
  disabled={
1588
  !isLoggedIn ||
1589
+ initBlockActions ||
1590
  (chatMode === "quiz" && !quizState.waitingForAnswer)
1591
  }
1592
  className="h-8 w-8 hover:bg-muted/50"
 
1602
  onChange={(e) => setInput(e.target.value)}
1603
  onKeyDown={handleKeyDown}
1604
  placeholder={
1605
+ initPlaceholder ??
1606
+ (!isLoggedIn
1607
  ? "Please log in on the right to start chatting..."
1608
  : chatMode === "quiz"
1609
  ? quizState.waitingForAnswer
 
1615
  ? "Type a message or drag files here... (mention @Clare to get AI assistance)"
1616
  : learningMode === "general"
1617
  ? "Ask me anything! Please provide context about your question..."
1618
+ : "Ask Clare anything about the course or drag files here...")
1619
  }
1620
  disabled={
1621
  !isLoggedIn ||
1622
+ initInputLocked || // key fix: DO NOT lock input during "asking"
1623
  (chatMode === "quiz" && !quizState.waitingForAnswer)
1624
  }
1625
  className={`min-h-[80px] pl-4 pr-20 resize-none bg-background border-2 ${
 
1632
  type="submit"
1633
  size="icon"
1634
  disabled={
1635
+ (!input.trim() && uploadedFiles.length === 0) ||
1636
  !isLoggedIn ||
1637
+ initInputLocked
 
 
1638
  }
1639
  className="h-8 w-8 rounded-full"
1640
  >
 
1649
  accept=".pdf,.docx,.pptx,.doc,.ppt,.jpg,.jpeg,.png,.gif,.webp"
1650
  onChange={handleFileSelect}
1651
  className="hidden"
1652
+ disabled={!isLoggedIn || initBlockActions}
1653
  />
1654
  </div>
1655
  </form>
1656
  </div>
1657
  </div>
1658
 
1659
+ {/* File Viewer Dialog */}
1660
  <Dialog
1661
  open={showFileViewer}
1662
  onOpenChange={(open) => {
 
1709
  </AlertDialogHeader>
1710
 
1711
  <AlertDialogFooter className="flex-col sm:flex-row gap-2 sm:justify-end">
1712
+ <Button variant="outline" onClick={() => onConfirmClear(false)} className="sm:flex-1 sm:max-w-[200px]">
 
 
 
 
1713
  Start New (Don't Save)
1714
  </Button>
1715
+ <AlertDialogAction onClick={() => onConfirmClear(true)} className="sm:flex-1 sm:max-w-[200px]">
 
 
 
1716
  Save & Start New
1717
  </AlertDialogAction>
1718
  </AlertDialogFooter>
 
1768
  <Checkbox
1769
  id="download-chat"
1770
  checked={downloadOptions.chat}
1771
+ onCheckedChange={(checked) => setDownloadOptions({ ...downloadOptions, chat: checked === true })}
 
 
1772
  />
1773
  <label htmlFor="download-chat" className="text-sm font-medium cursor-pointer">
1774
  Download chat
 
1778
  <Checkbox
1779
  id="download-summary"
1780
  checked={downloadOptions.summary}
1781
+ onCheckedChange={(checked) => setDownloadOptions({ ...downloadOptions, summary: checked === true })}
 
 
1782
  />
1783
  <label htmlFor="download-summary" className="text-sm font-medium cursor-pointer">
1784
  Download summary
 
1867
  </AlertDialogContent>
1868
  </AlertDialog>
1869
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1870
  {/* File Type Selection Dialog */}
1871
  {showTypeDialog && (
1872
  <Dialog open={showTypeDialog} onOpenChange={setShowTypeDialog}>
1873
  <DialogContent className="sm:max-w-[425px]" style={{ zIndex: 99999 }}>
1874
  <DialogHeader>
1875
  <DialogTitle>Select File Types</DialogTitle>
1876
+ <DialogDescription>Please select the type for each file you are uploading.</DialogDescription>
 
 
1877
  </DialogHeader>
1878
 
1879
  <div className="space-y-3 max-h-64 overflow-y-auto">
 
1885
  <Icon className="h-4 w-4 text-muted-foreground flex-shrink-0" />
1886
  <div className="flex-1 min-w-0">
1887
  <p className="text-sm truncate">{pendingFile.file.name}</p>
1888
+ <p className="text-xs text-muted-foreground">{formatFileSize(pendingFile.file.size)}</p>
 
 
1889
  </div>
1890
  </div>
1891
 
 
1893
  <label className="text-xs text-muted-foreground">File Type</label>
1894
  <Select
1895
  value={pendingFile.type}
1896
+ onValueChange={(value) => handlePendingFileTypeChange(index, value as FileType)}
 
 
1897
  >
1898
  <SelectTrigger className="h-8 text-xs">
1899
  <SelectValue />
1900
  </SelectTrigger>
1901
+ <SelectContent className="!z-[100000] !bg-background !text-foreground" style={{ zIndex: 100000 }}>
 
 
 
1902
  <SelectItem value="syllabus">Syllabus</SelectItem>
1903
+ <SelectItem value="lecture-slides">Lecture Slides / PPT</SelectItem>
1904
+ <SelectItem value="literature-review">Literature Review / Paper</SelectItem>
 
 
 
 
1905
  <SelectItem value="other">Other Course Document</SelectItem>
1906
  </SelectContent>
1907
  </Select>