Reubencf Claude commited on
Commit
7f6d612
Β·
1 Parent(s): d7409d2

Fix quiz system: separate answers, backward compat, and bug fixes

Browse files

- Quiz.json no longer contains correct answers (prevents cheating)
- Correct answers saved to quiz_key.json for grading
- Fixed analyzeQuiz to read userAnswersData.metadata (was answersData)
- Added backward compatibility for quizzes without quiz_key.json
- Made all components responsive (Messages, FileManager, TextEditor, etc.)

πŸ€– Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>

.claude/settings.local.json CHANGED
@@ -32,7 +32,8 @@
32
  "Bash(REUBENOS_URL=https://huggingface.co/spaces/MCP-1st-Birthday/Reuben_OS node test-api.js:*)",
33
  "Bash(curl:*)",
34
  "Bash(REUBENOS_URL=https://mcp-1st-birthday-reuben-os.hf.space node test-api.js:*)",
35
- "Bash(node test-download.js:*)"
 
36
  ],
37
  "deny": [],
38
  "ask": []
 
32
  "Bash(REUBENOS_URL=https://huggingface.co/spaces/MCP-1st-Birthday/Reuben_OS node test-api.js:*)",
33
  "Bash(curl:*)",
34
  "Bash(REUBENOS_URL=https://mcp-1st-birthday-reuben-os.hf.space node test-api.js:*)",
35
+ "Bash(node test-download.js:*)",
36
+ "Bash(npm show:*)"
37
  ],
38
  "deny": [],
39
  "ask": []
app/components/AboutModal.tsx CHANGED
@@ -24,18 +24,18 @@ export function AboutModal({ isOpen, onClose }: AboutModalProps) {
24
  transition={{ duration: 0.2 }}
25
  className="fixed top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 z-[101]"
26
  >
27
- <div className="bg-white/90 backdrop-blur-xl rounded-2xl shadow-2xl p-8 w-[400px] border border-white/40 text-center relative">
28
  {/* Logo */}
29
- <div className="w-24 h-24 mx-auto mb-6 bg-gradient-to-br from-purple-600 to-blue-600 rounded-2xl flex items-center justify-center shadow-lg">
30
- <span className="text-white font-bold text-4xl">R</span>
31
  </div>
32
 
33
  {/* Title */}
34
- <h1 className="text-3xl font-bold text-gray-900 mb-2">Reuben OS</h1>
35
- <p className="text-gray-500 text-sm mb-6">Version 1.0.0</p>
36
 
37
  {/* Creator Info */}
38
- <div className="text-gray-600 font-medium text-sm">
39
  Created by a huggingface user <a href="https://huggingface.co/Reubencf" target="_blank" rel="noopener noreferrer" className="text-blue-500 hover:text-blue-600 hover:underline transition-colors">Reubencf</a>
40
  </div>
41
 
@@ -43,9 +43,9 @@ export function AboutModal({ isOpen, onClose }: AboutModalProps) {
43
  {/* Close Button */}
44
  <button
45
  onClick={onClose}
46
- className="absolute top-4 right-4 text-gray-400 hover:text-gray-600 transition-colors"
47
  >
48
- <X size={20} weight="bold" />
49
  </button>
50
  </div>
51
  </motion.div>
 
24
  transition={{ duration: 0.2 }}
25
  className="fixed top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 z-[101]"
26
  >
27
+ <div className="bg-white/90 backdrop-blur-xl rounded-2xl shadow-2xl p-4 sm:p-8 w-[90vw] max-w-[400px] border border-white/40 text-center relative mx-4">
28
  {/* Logo */}
29
+ <div className="w-16 h-16 sm:w-24 sm:h-24 mx-auto mb-4 sm:mb-6 bg-gradient-to-br from-purple-600 to-blue-600 rounded-xl sm:rounded-2xl flex items-center justify-center shadow-lg">
30
+ <span className="text-white font-bold text-2xl sm:text-4xl">R</span>
31
  </div>
32
 
33
  {/* Title */}
34
+ <h1 className="text-xl sm:text-3xl font-bold text-gray-900 mb-1 sm:mb-2">Reuben OS</h1>
35
+ <p className="text-gray-500 text-xs sm:text-sm mb-4 sm:mb-6">Version 1.0.0</p>
36
 
37
  {/* Creator Info */}
38
+ <div className="text-gray-600 font-medium text-xs sm:text-sm">
39
  Created by a huggingface user <a href="https://huggingface.co/Reubencf" target="_blank" rel="noopener noreferrer" className="text-blue-500 hover:text-blue-600 hover:underline transition-colors">Reubencf</a>
40
  </div>
41
 
 
43
  {/* Close Button */}
44
  <button
45
  onClick={onClose}
46
+ className="absolute top-3 right-3 sm:top-4 sm:right-4 text-gray-400 hover:text-gray-600 transition-colors"
47
  >
48
+ <X size={18} weight="bold" className="sm:w-5 sm:h-5" />
49
  </button>
50
  </div>
51
  </motion.div>
app/components/FileManager.tsx CHANGED
@@ -35,7 +35,8 @@ import {
35
  Lightning,
36
  Brain,
37
  Lock,
38
- Key
 
39
  } from '@phosphor-icons/react'
40
  import { motion, AnimatePresence } from 'framer-motion'
41
  import { FilePreview } from './FilePreview'
@@ -73,6 +74,7 @@ export function FileManager({ currentPath, onNavigate, onClose, onOpenFlutterApp
73
  const [uploadModalOpen, setUploadModalOpen] = useState(false)
74
  const [previewFile, setPreviewFile] = useState<FileItem | null>(null)
75
  const [sidebarSelection, setSidebarSelection] = useState('public') // Default to public
 
76
 
77
  // Secure Data State
78
  const [passkey, setPasskey] = useState('')
@@ -383,106 +385,137 @@ export function FileManager({ currentPath, onNavigate, onClose, onOpenFlutterApp
383
  y={60}
384
  className="file-manager-window"
385
  >
386
- <div className="flex h-full bg-[#F5F5F5]">
 
 
 
 
 
 
 
 
 
 
 
 
 
387
  {/* Sidebar */}
388
- <div className="w-48 bg-[#F3F3F3]/90 backdrop-blur-xl border-r border-gray-200 pt-4 flex flex-col">
389
- <div className="px-4 mb-2">
390
- <span className="text-xs font-bold text-gray-400">Locations</span>
391
- </div>
392
- <nav className="space-y-1 px-2">
393
- <button
394
- onClick={() => handleSidebarClick('public')}
395
- className={`w-full flex items-center gap-2 px-2 py-1.5 rounded-md text-sm ${sidebarSelection === 'public' ? 'bg-gray-200 text-black' : 'text-gray-600 hover:bg-gray-100'}`}
396
- >
397
- <Globe size={18} weight="fill" className="text-purple-500" />
398
- Public Files
399
- </button>
400
- <button
401
- onClick={() => handleSidebarClick('secure')}
402
- className={`w-full flex items-center gap-2 px-2 py-1.5 rounded-md text-sm ${sidebarSelection === 'secure' ? 'bg-gray-200 text-black' : 'text-gray-600 hover:bg-gray-100'}`}
403
- >
404
- <Lock size={18} weight="fill" className="text-blue-500" />
405
- Secure Data
406
- </button>
407
- <button
408
- onClick={() => handleSidebarClick('applications')}
409
- className={`w-full flex items-center gap-2 px-2 py-1.5 rounded-md text-sm ${sidebarSelection === 'applications' ? 'bg-gray-200 text-black' : 'text-gray-600 hover:bg-gray-100'}`}
410
  >
411
- <AppWindow size={18} weight="fill" className="text-green-500" />
412
- Applications
413
- </button>
414
- </nav>
415
- </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
416
 
417
  {/* Main Content */}
418
- <div className="flex-1 flex flex-col bg-white relative">
419
  {/* Toolbar */}
420
- <div className="h-12 bg-white border-b border-gray-200 flex items-center px-4 gap-4 justify-between">
421
- <div className="flex items-center gap-2">
 
 
 
 
 
 
422
  <button
423
  onClick={() => {
424
  const parent = currentPath.split('/').slice(0, -1).join('/')
425
  onNavigate(parent)
426
  }}
427
  disabled={!currentPath || currentPath === 'Applications'}
428
- className="p-1.5 hover:bg-gray-100 rounded-md disabled:opacity-30 transition-colors"
429
  >
430
- <CaretLeft size={18} weight="bold" className="text-gray-600" />
431
  </button>
432
- <span className="text-sm font-semibold text-gray-700">
433
  {currentPath === '' ? (sidebarSelection === 'secure' ? 'Secure Data' : 'Public') : currentPath}
434
  </span>
435
  </div>
436
 
437
- <div className="flex items-center gap-2">
438
  {sidebarSelection === 'secure' && passkey && (
439
  <button
440
  onClick={handleLock}
441
- className="flex items-center gap-1 px-3 py-1.5 bg-gray-100 hover:bg-gray-200 rounded-md text-xs font-medium text-gray-600 transition-colors"
442
  >
443
- <Lock size={14} />
444
- Lock
445
  </button>
446
  )}
447
 
448
  {currentPath !== 'Applications' && (
449
- <>
450
- <button
451
- onClick={() => setUploadModalOpen(true)}
452
- className="p-1.5 hover:bg-gray-100 rounded-md transition-colors"
453
- title="Upload File"
454
- >
455
- <Upload size={18} weight="bold" className="text-gray-600" />
456
- </button>
457
- </>
458
  )}
459
- <div className="flex items-center gap-2 px-3 py-1.5 bg-gray-100 rounded-md w-48">
460
- <MagnifyingGlass size={14} weight="bold" className="text-gray-400" />
461
  <input
462
  type="text"
463
  placeholder="Search"
464
  value={searchQuery}
465
  onChange={(e) => setSearchQuery(e.target.value)}
466
- className="bg-transparent text-sm outline-none w-full placeholder-gray-400"
467
  />
468
  </div>
469
  </div>
470
  </div>
471
 
472
  {/* File Grid */}
473
- <div className="flex-1 overflow-auto p-6">
474
  {currentPath === 'Applications' ? (
475
- <div className="grid grid-cols-5 gap-6">
476
  {applications.map((app) => (
477
  <button
478
  key={app.id}
479
  onDoubleClick={() => onOpenApp && onOpenApp(app.id)}
480
- className="flex flex-col items-center gap-3 p-4 hover:bg-blue-50 rounded-xl transition-colors group"
481
  >
482
- <div className="w-16 h-16 flex items-center justify-center drop-shadow-sm group-hover:scale-105 transition-transform">
483
  {app.icon}
484
  </div>
485
- <span className="text-sm font-medium text-gray-700">{app.name}</span>
486
  </button>
487
  ))}
488
  </div>
@@ -493,11 +526,11 @@ export function FileManager({ currentPath, onNavigate, onClose, onOpenFlutterApp
493
  Loading files...
494
  </div>
495
  ) : filteredFiles.length === 0 ? (
496
- <div className="flex items-center justify-center h-full text-gray-400 text-sm">
497
  {searchQuery ? 'No files found' : 'Folder is empty'}
498
  </div>
499
  ) : (
500
- <div className="grid grid-cols-5 gap-6">
501
  {filteredFiles.map((file) => (
502
  <div
503
  key={file.path}
@@ -588,42 +621,42 @@ export function FileManager({ currentPath, onNavigate, onClose, onOpenFlutterApp
588
  handlePreview(file)
589
  }
590
  }}
591
- className="flex flex-col items-center gap-3 p-4 hover:bg-blue-50 rounded-xl w-full transition-colors"
592
  >
593
- <div className="w-16 h-16 flex items-center justify-center drop-shadow-sm">
594
  {getFileIcon(file)}
595
  </div>
596
- <span className="text-sm font-medium text-gray-700 text-center break-all w-full line-clamp-2">
597
  {file.name}
598
  </span>
599
  {file.size && (
600
- <span className="text-xs text-gray-400">
601
  {formatFileSize(file.size)}
602
  </span>
603
  )}
604
  </button>
605
 
606
  {file.type === 'file' && (
607
- <div className="absolute top-2 right-2 hidden group-hover:flex gap-1 bg-white/90 rounded-lg shadow-sm p-1">
608
  <button
609
  onClick={(e) => {
610
  e.stopPropagation()
611
  handlePreview(file)
612
  }}
613
- className="p-1.5 hover:bg-gray-100 rounded-md text-gray-600"
614
  title="Preview"
615
  >
616
- <Eye size={14} weight="bold" />
617
  </button>
618
  <button
619
  onClick={(e) => {
620
  e.stopPropagation()
621
  handleDelete(file)
622
  }}
623
- className="p-1.5 hover:bg-red-50 rounded-md text-red-500"
624
  title="Delete"
625
  >
626
- <Trash size={14} weight="bold" />
627
  </button>
628
  </div>
629
  )}
@@ -636,7 +669,7 @@ export function FileManager({ currentPath, onNavigate, onClose, onOpenFlutterApp
636
  </div>
637
 
638
  {/* Status Bar */}
639
- <div className="h-8 bg-white border-t border-gray-200 flex items-center px-4 text-xs text-gray-500 font-medium">
640
  {currentPath === 'Applications'
641
  ? `${applications.length} items`
642
  : `${filteredFiles.length} items`
@@ -650,18 +683,18 @@ export function FileManager({ currentPath, onNavigate, onClose, onOpenFlutterApp
650
  initial={{ opacity: 0 }}
651
  animate={{ opacity: 1 }}
652
  exit={{ opacity: 0 }}
653
- className="absolute inset-0 bg-white/80 backdrop-blur-md z-50 flex items-center justify-center"
654
  >
655
  <motion.div
656
  initial={{ scale: 0.9, y: 20 }}
657
  animate={{ scale: 1, y: 0 }}
658
- className="bg-white p-8 rounded-2xl shadow-2xl border border-gray-200 w-96 text-center"
659
  >
660
- <div className="w-16 h-16 bg-blue-50 rounded-full flex items-center justify-center mx-auto mb-4">
661
- <Lock size={32} weight="fill" className="text-blue-500" />
662
  </div>
663
- <h3 className="text-xl font-bold text-gray-900 mb-2">Secure Storage</h3>
664
- <p className="text-gray-500 text-sm mb-6">Enter your passkey to access your files.</p>
665
 
666
  <input
667
  type="password"
@@ -669,18 +702,18 @@ export function FileManager({ currentPath, onNavigate, onClose, onOpenFlutterApp
669
  onChange={(e) => setTempPasskey(e.target.value)}
670
  onKeyDown={(e) => e.key === 'Enter' && handlePasskeySubmit()}
671
  placeholder="Enter Passkey"
672
- className="w-full px-4 py-3 rounded-xl border border-gray-300 focus:border-blue-500 focus:ring-2 focus:ring-blue-200 outline-none mb-4 text-center text-lg tracking-widest"
673
  autoFocus
674
  />
675
 
676
- <div className="flex gap-3">
677
  <button
678
  onClick={() => {
679
  setShowPasskeyModal(false)
680
  setSidebarSelection('public')
681
  onNavigate('public')
682
  }}
683
- className="flex-1 px-4 py-2.5 bg-gray-100 text-gray-700 rounded-xl font-medium hover:bg-gray-200 transition-colors"
684
  >
685
  Cancel
686
  </button>
 
35
  Lightning,
36
  Brain,
37
  Lock,
38
+ Key,
39
+ List
40
  } from '@phosphor-icons/react'
41
  import { motion, AnimatePresence } from 'framer-motion'
42
  import { FilePreview } from './FilePreview'
 
74
  const [uploadModalOpen, setUploadModalOpen] = useState(false)
75
  const [previewFile, setPreviewFile] = useState<FileItem | null>(null)
76
  const [sidebarSelection, setSidebarSelection] = useState('public') // Default to public
77
+ const [mobileSidebarOpen, setMobileSidebarOpen] = useState(false)
78
 
79
  // Secure Data State
80
  const [passkey, setPasskey] = useState('')
 
385
  y={60}
386
  className="file-manager-window"
387
  >
388
+ <div className="flex h-full bg-[#F5F5F5] relative">
389
+ {/* Mobile Sidebar Overlay */}
390
+ <AnimatePresence>
391
+ {mobileSidebarOpen && (
392
+ <motion.div
393
+ initial={{ opacity: 0 }}
394
+ animate={{ opacity: 1 }}
395
+ exit={{ opacity: 0 }}
396
+ className="absolute inset-0 bg-black/30 z-20 md:hidden"
397
+ onClick={() => setMobileSidebarOpen(false)}
398
+ />
399
+ )}
400
+ </AnimatePresence>
401
+
402
  {/* Sidebar */}
403
+ <AnimatePresence>
404
+ {(mobileSidebarOpen || typeof window !== 'undefined') && (
405
+ <motion.div
406
+ initial={{ x: -192 }}
407
+ animate={{ x: mobileSidebarOpen ? 0 : (typeof window !== 'undefined' && window.innerWidth >= 768 ? 0 : -192) }}
408
+ className={`${mobileSidebarOpen ? 'absolute z-30 h-full' : 'hidden'} md:relative md:flex w-40 sm:w-48 bg-[#F3F3F3]/95 md:bg-[#F3F3F3]/90 backdrop-blur-xl border-r border-gray-200 pt-3 sm:pt-4 flex-col`}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
409
  >
410
+ <div className="px-3 sm:px-4 mb-2 flex items-center justify-between">
411
+ <span className="text-[10px] sm:text-xs font-bold text-gray-400">Locations</span>
412
+ <button
413
+ onClick={() => setMobileSidebarOpen(false)}
414
+ className="p-1 hover:bg-gray-200 rounded md:hidden"
415
+ >
416
+ <X size={14} />
417
+ </button>
418
+ </div>
419
+ <nav className="space-y-1 px-1.5 sm:px-2">
420
+ <button
421
+ onClick={() => { handleSidebarClick('public'); setMobileSidebarOpen(false); }}
422
+ className={`w-full flex items-center gap-1.5 sm:gap-2 px-2 py-1.5 rounded-md text-xs sm:text-sm ${sidebarSelection === 'public' ? 'bg-gray-200 text-black' : 'text-gray-600 hover:bg-gray-100'}`}
423
+ >
424
+ <Globe size={16} weight="fill" className="text-purple-500 sm:w-[18px] sm:h-[18px]" />
425
+ <span className="truncate">Public Files</span>
426
+ </button>
427
+ <button
428
+ onClick={() => { handleSidebarClick('secure'); setMobileSidebarOpen(false); }}
429
+ className={`w-full flex items-center gap-1.5 sm:gap-2 px-2 py-1.5 rounded-md text-xs sm:text-sm ${sidebarSelection === 'secure' ? 'bg-gray-200 text-black' : 'text-gray-600 hover:bg-gray-100'}`}
430
+ >
431
+ <Lock size={16} weight="fill" className="text-blue-500 sm:w-[18px] sm:h-[18px]" />
432
+ <span className="truncate">Secure Data</span>
433
+ </button>
434
+ <button
435
+ onClick={() => { handleSidebarClick('applications'); setMobileSidebarOpen(false); }}
436
+ className={`w-full flex items-center gap-1.5 sm:gap-2 px-2 py-1.5 rounded-md text-xs sm:text-sm ${sidebarSelection === 'applications' ? 'bg-gray-200 text-black' : 'text-gray-600 hover:bg-gray-100'}`}
437
+ >
438
+ <AppWindow size={16} weight="fill" className="text-green-500 sm:w-[18px] sm:h-[18px]" />
439
+ <span className="truncate">Applications</span>
440
+ </button>
441
+ </nav>
442
+ </motion.div>
443
+ )}
444
+ </AnimatePresence>
445
 
446
  {/* Main Content */}
447
+ <div className="flex-1 flex flex-col bg-white relative min-w-0">
448
  {/* Toolbar */}
449
+ <div className="h-10 sm:h-12 bg-white border-b border-gray-200 flex items-center px-2 sm:px-4 gap-2 sm:gap-4 justify-between">
450
+ <div className="flex items-center gap-1 sm:gap-2 min-w-0 flex-1">
451
+ <button
452
+ onClick={() => setMobileSidebarOpen(true)}
453
+ className="p-1.5 hover:bg-gray-100 rounded-md md:hidden flex-shrink-0"
454
+ >
455
+ <List size={18} weight="bold" className="text-gray-600" />
456
+ </button>
457
  <button
458
  onClick={() => {
459
  const parent = currentPath.split('/').slice(0, -1).join('/')
460
  onNavigate(parent)
461
  }}
462
  disabled={!currentPath || currentPath === 'Applications'}
463
+ className="p-1 sm:p-1.5 hover:bg-gray-100 rounded-md disabled:opacity-30 transition-colors flex-shrink-0"
464
  >
465
+ <CaretLeft size={16} weight="bold" className="text-gray-600 sm:w-[18px] sm:h-[18px]" />
466
  </button>
467
+ <span className="text-xs sm:text-sm font-semibold text-gray-700 truncate">
468
  {currentPath === '' ? (sidebarSelection === 'secure' ? 'Secure Data' : 'Public') : currentPath}
469
  </span>
470
  </div>
471
 
472
+ <div className="flex items-center gap-1 sm:gap-2 flex-shrink-0">
473
  {sidebarSelection === 'secure' && passkey && (
474
  <button
475
  onClick={handleLock}
476
+ className="hidden xs:flex items-center gap-1 px-2 sm:px-3 py-1 sm:py-1.5 bg-gray-100 hover:bg-gray-200 rounded-md text-[10px] sm:text-xs font-medium text-gray-600 transition-colors"
477
  >
478
+ <Lock size={12} className="sm:w-3.5 sm:h-3.5" />
479
+ <span className="hidden sm:inline">Lock</span>
480
  </button>
481
  )}
482
 
483
  {currentPath !== 'Applications' && (
484
+ <button
485
+ onClick={() => setUploadModalOpen(true)}
486
+ className="p-1 sm:p-1.5 hover:bg-gray-100 rounded-md transition-colors"
487
+ title="Upload File"
488
+ >
489
+ <Upload size={16} weight="bold" className="text-gray-600 sm:w-[18px] sm:h-[18px]" />
490
+ </button>
 
 
491
  )}
492
+ <div className="flex items-center gap-1 sm:gap-2 px-2 sm:px-3 py-1 sm:py-1.5 bg-gray-100 rounded-md w-24 sm:w-32 md:w-48">
493
+ <MagnifyingGlass size={12} weight="bold" className="text-gray-400 sm:w-3.5 sm:h-3.5 flex-shrink-0" />
494
  <input
495
  type="text"
496
  placeholder="Search"
497
  value={searchQuery}
498
  onChange={(e) => setSearchQuery(e.target.value)}
499
+ className="bg-transparent text-xs sm:text-sm outline-none w-full placeholder-gray-400 min-w-0"
500
  />
501
  </div>
502
  </div>
503
  </div>
504
 
505
  {/* File Grid */}
506
+ <div className="flex-1 overflow-auto p-3 sm:p-6">
507
  {currentPath === 'Applications' ? (
508
+ <div className="grid grid-cols-3 xs:grid-cols-4 sm:grid-cols-4 md:grid-cols-5 gap-2 sm:gap-4 md:gap-6">
509
  {applications.map((app) => (
510
  <button
511
  key={app.id}
512
  onDoubleClick={() => onOpenApp && onOpenApp(app.id)}
513
+ className="flex flex-col items-center gap-1.5 sm:gap-3 p-2 sm:p-4 hover:bg-blue-50 rounded-xl transition-colors group"
514
  >
515
+ <div className="w-10 h-10 sm:w-14 sm:h-14 md:w-16 md:h-16 flex items-center justify-center drop-shadow-sm group-hover:scale-105 transition-transform">
516
  {app.icon}
517
  </div>
518
+ <span className="text-[10px] sm:text-xs md:text-sm font-medium text-gray-700 text-center line-clamp-2">{app.name}</span>
519
  </button>
520
  ))}
521
  </div>
 
526
  Loading files...
527
  </div>
528
  ) : filteredFiles.length === 0 ? (
529
+ <div className="flex items-center justify-center h-full text-gray-400 text-xs sm:text-sm">
530
  {searchQuery ? 'No files found' : 'Folder is empty'}
531
  </div>
532
  ) : (
533
+ <div className="grid grid-cols-3 xs:grid-cols-4 sm:grid-cols-4 md:grid-cols-5 gap-2 sm:gap-4 md:gap-6">
534
  {filteredFiles.map((file) => (
535
  <div
536
  key={file.path}
 
621
  handlePreview(file)
622
  }
623
  }}
624
+ className="flex flex-col items-center gap-1.5 sm:gap-3 p-2 sm:p-4 hover:bg-blue-50 rounded-xl w-full transition-colors"
625
  >
626
+ <div className="w-10 h-10 sm:w-14 sm:h-14 md:w-16 md:h-16 flex items-center justify-center drop-shadow-sm">
627
  {getFileIcon(file)}
628
  </div>
629
+ <span className="text-[10px] sm:text-xs md:text-sm font-medium text-gray-700 text-center break-all w-full line-clamp-2">
630
  {file.name}
631
  </span>
632
  {file.size && (
633
+ <span className="text-[9px] sm:text-xs text-gray-400 hidden sm:block">
634
  {formatFileSize(file.size)}
635
  </span>
636
  )}
637
  </button>
638
 
639
  {file.type === 'file' && (
640
+ <div className="absolute top-1 right-1 sm:top-2 sm:right-2 hidden group-hover:flex gap-0.5 sm:gap-1 bg-white/90 rounded-lg shadow-sm p-0.5 sm:p-1">
641
  <button
642
  onClick={(e) => {
643
  e.stopPropagation()
644
  handlePreview(file)
645
  }}
646
+ className="p-1 sm:p-1.5 hover:bg-gray-100 rounded-md text-gray-600"
647
  title="Preview"
648
  >
649
+ <Eye size={12} weight="bold" className="sm:w-3.5 sm:h-3.5" />
650
  </button>
651
  <button
652
  onClick={(e) => {
653
  e.stopPropagation()
654
  handleDelete(file)
655
  }}
656
+ className="p-1 sm:p-1.5 hover:bg-red-50 rounded-md text-red-500"
657
  title="Delete"
658
  >
659
+ <Trash size={12} weight="bold" className="sm:w-3.5 sm:h-3.5" />
660
  </button>
661
  </div>
662
  )}
 
669
  </div>
670
 
671
  {/* Status Bar */}
672
+ <div className="h-6 sm:h-8 bg-white border-t border-gray-200 flex items-center px-2 sm:px-4 text-[10px] sm:text-xs text-gray-500 font-medium">
673
  {currentPath === 'Applications'
674
  ? `${applications.length} items`
675
  : `${filteredFiles.length} items`
 
683
  initial={{ opacity: 0 }}
684
  animate={{ opacity: 1 }}
685
  exit={{ opacity: 0 }}
686
+ className="absolute inset-0 bg-white/80 backdrop-blur-md z-50 flex items-center justify-center p-4"
687
  >
688
  <motion.div
689
  initial={{ scale: 0.9, y: 20 }}
690
  animate={{ scale: 1, y: 0 }}
691
+ className="bg-white p-4 sm:p-8 rounded-2xl shadow-2xl border border-gray-200 w-full max-w-xs sm:max-w-sm text-center"
692
  >
693
+ <div className="w-12 h-12 sm:w-16 sm:h-16 bg-blue-50 rounded-full flex items-center justify-center mx-auto mb-3 sm:mb-4">
694
+ <Lock size={24} weight="fill" className="text-blue-500 sm:w-8 sm:h-8" />
695
  </div>
696
+ <h3 className="text-lg sm:text-xl font-bold text-gray-900 mb-1 sm:mb-2">Secure Storage</h3>
697
+ <p className="text-gray-500 text-xs sm:text-sm mb-4 sm:mb-6">Enter your passkey to access your files.</p>
698
 
699
  <input
700
  type="password"
 
702
  onChange={(e) => setTempPasskey(e.target.value)}
703
  onKeyDown={(e) => e.key === 'Enter' && handlePasskeySubmit()}
704
  placeholder="Enter Passkey"
705
+ className="w-full px-3 sm:px-4 py-2 sm:py-3 rounded-xl border border-gray-300 focus:border-blue-500 focus:ring-2 focus:ring-blue-200 outline-none mb-3 sm:mb-4 text-center text-base sm:text-lg tracking-widest"
706
  autoFocus
707
  />
708
 
709
+ <div className="flex gap-2 sm:gap-3">
710
  <button
711
  onClick={() => {
712
  setShowPasskeyModal(false)
713
  setSidebarSelection('public')
714
  onNavigate('public')
715
  }}
716
+ className="flex-1 px-3 sm:px-4 py-2 sm:py-2.5 bg-gray-100 text-gray-700 rounded-xl text-sm sm:text-base font-medium hover:bg-gray-200 transition-colors"
717
  >
718
  Cancel
719
  </button>
app/components/HelpModal.tsx CHANGED
@@ -63,56 +63,56 @@ export function HelpModal({ isOpen, onClose }: HelpModalProps) {
63
  animate={{ scale: 1, opacity: 1 }}
64
  exit={{ scale: 0.9, opacity: 0 }}
65
  transition={{ type: "spring", damping: 20, stiffness: 300 }}
66
- className="fixed w-[600px] bg-white rounded-lg shadow-2xl z-50 select-none"
67
  >
68
  {/* Ubuntu-style Window Header */}
69
  <div
70
  onMouseDown={handleMouseDown}
71
- className="h-11 bg-gradient-to-b from-[#f6f5f4] to-[#edebe9] border-b border-[#d0d0d0] flex items-center justify-between px-3 cursor-move rounded-t-lg"
72
  >
73
- <div className="flex items-center gap-2 flex-1">
74
- <div className="flex items-center gap-1 window-controls">
75
  <button
76
  onClick={onClose}
77
- className="w-5 h-5 rounded-full bg-[#E95420] hover:bg-[#d14818] flex items-center justify-center group"
78
  >
79
- <X size={12} weight="bold" className="text-white opacity-0 group-hover:opacity-100" />
80
  </button>
81
- <button className="w-5 h-5 rounded-full bg-[#ddd] hover:bg-[#ccc] flex items-center justify-center group">
82
- <Minus size={12} weight="bold" className="text-[#666] opacity-0 group-hover:opacity-100" />
83
  </button>
84
- <button className="w-5 h-5 rounded-full bg-[#ddd] hover:bg-[#ccc] flex items-center justify-center group">
85
- <Square size={10} weight="bold" className="text-[#666] opacity-0 group-hover:opacity-100" />
86
  </button>
87
  </div>
88
- <div className="flex items-center gap-2 ml-2">
89
- <Info size={18} weight="fill" className="text-[#E95420]" />
90
- <span className="text-sm font-medium text-[#2c2c2c]">About This Application</span>
91
  </div>
92
  </div>
93
  </div>
94
 
95
  {/* Content */}
96
- <div className="p-6 space-y-5 bg-white rounded-b-lg">
97
  {/* Creator Info */}
98
- <div className="flex items-start gap-4 p-4 bg-gradient-to-r from-[#E95420]/10 to-orange-100 rounded-lg border border-[#E95420]/20">
99
- <div className="w-10 h-10 rounded-full bg-gradient-to-br from-[#E95420] to-[#d14818] flex items-center justify-center flex-shrink-0">
100
- <UserCircle size={24} weight="fill" className="text-white" />
101
  </div>
102
- <div>
103
- <h3 className="text-base font-semibold text-[#2c2c2c] mb-1">Created By</h3>
104
- <p className="text-[#555] font-medium">Reuben Chagas Fernandes</p>
105
  </div>
106
  </div>
107
 
108
  {/* Purpose */}
109
- <div className="flex items-start gap-4 p-4 bg-blue-50 rounded-lg border border-blue-200">
110
- <div className="w-10 h-10 rounded-full bg-gradient-to-br from-blue-500 to-blue-600 flex items-center justify-center flex-shrink-0">
111
- <Target size={24} weight="fill" className="text-white" />
112
  </div>
113
- <div>
114
- <h3 className="text-base font-semibold text-[#2c2c2c] mb-2">Purpose</h3>
115
- <p className="text-[#555] leading-relaxed text-sm">
116
  This application was created for sharing study material and making it easily
117
  accessible through Claude. Our goal is to provide a seamless platform for
118
  students to collaborate, share resources, and enhance their learning experience.
@@ -121,44 +121,44 @@ export function HelpModal({ isOpen, onClose }: HelpModalProps) {
121
  </div>
122
 
123
  {/* Features */}
124
- <div className="bg-gradient-to-br from-purple-50 to-pink-50 rounded-lg p-4 border border-purple-200">
125
- <h4 className="text-sm font-semibold text-[#2c2c2c] uppercase tracking-wider mb-3 flex items-center gap-2">
126
- <span className="w-1 h-4 bg-[#E95420] rounded-full"></span>
127
  Key Features
128
  </h4>
129
- <ul className="space-y-2.5 text-[#555]">
130
- <li className="flex items-center gap-3">
131
- <span className="w-2 h-2 bg-blue-500 rounded-full flex-shrink-0" />
132
- <span className="text-sm">Easy file upload and sharing</span>
133
  </li>
134
- <li className="flex items-center gap-3">
135
- <span className="w-2 h-2 bg-purple-500 rounded-full flex-shrink-0" />
136
- <span className="text-sm">Integration with Claude AI</span>
137
  </li>
138
- <li className="flex items-center gap-3">
139
- <span className="w-2 h-2 bg-green-500 rounded-full flex-shrink-0" />
140
- <span className="text-sm">Public folder for community sharing</span>
141
  </li>
142
- <li className="flex items-center gap-3">
143
- <span className="w-2 h-2 bg-[#E95420] rounded-full flex-shrink-0" />
144
- <span className="text-sm">Exam calendar and organization tools</span>
145
  </li>
146
- <li className="flex items-center gap-3">
147
- <span className="w-2 h-2 bg-cyan-500 rounded-full flex-shrink-0" />
148
- <span className="text-sm">Web browser with CORS proxy support</span>
149
  </li>
150
- <li className="flex items-center gap-3">
151
- <span className="w-2 h-2 bg-orange-500 rounded-full flex-shrink-0" />
152
- <span className="text-sm">Gemini AI chat assistant</span>
153
  </li>
154
  </ul>
155
  </div>
156
 
157
  {/* Footer Button */}
158
- <div className="pt-2">
159
  <button
160
  onClick={onClose}
161
- className="w-full py-2.5 bg-[#E95420] hover:bg-[#d14818] text-white rounded-lg font-medium transition-colors shadow-sm"
162
  >
163
  Close
164
  </button>
 
63
  animate={{ scale: 1, opacity: 1 }}
64
  exit={{ scale: 0.9, opacity: 0 }}
65
  transition={{ type: "spring", damping: 20, stiffness: 300 }}
66
+ className="fixed w-[95vw] max-w-[600px] bg-white rounded-lg shadow-2xl z-50 select-none max-h-[90vh] overflow-hidden flex flex-col"
67
  >
68
  {/* Ubuntu-style Window Header */}
69
  <div
70
  onMouseDown={handleMouseDown}
71
+ className="h-10 sm:h-11 bg-gradient-to-b from-[#f6f5f4] to-[#edebe9] border-b border-[#d0d0d0] flex items-center justify-between px-2 sm:px-3 cursor-move rounded-t-lg flex-shrink-0"
72
  >
73
+ <div className="flex items-center gap-1.5 sm:gap-2 flex-1 min-w-0">
74
+ <div className="flex items-center gap-1 window-controls flex-shrink-0">
75
  <button
76
  onClick={onClose}
77
+ className="w-4 h-4 sm:w-5 sm:h-5 rounded-full bg-[#E95420] hover:bg-[#d14818] flex items-center justify-center group"
78
  >
79
+ <X size={10} weight="bold" className="text-white opacity-0 group-hover:opacity-100 sm:w-3 sm:h-3" />
80
  </button>
81
+ <button className="w-4 h-4 sm:w-5 sm:h-5 rounded-full bg-[#ddd] hover:bg-[#ccc] flex items-center justify-center group">
82
+ <Minus size={10} weight="bold" className="text-[#666] opacity-0 group-hover:opacity-100 sm:w-3 sm:h-3" />
83
  </button>
84
+ <button className="w-4 h-4 sm:w-5 sm:h-5 rounded-full bg-[#ddd] hover:bg-[#ccc] flex items-center justify-center group">
85
+ <Square size={8} weight="bold" className="text-[#666] opacity-0 group-hover:opacity-100 sm:w-2.5 sm:h-2.5" />
86
  </button>
87
  </div>
88
+ <div className="flex items-center gap-1.5 sm:gap-2 ml-1 sm:ml-2 min-w-0">
89
+ <Info size={16} weight="fill" className="text-[#E95420] sm:w-[18px] sm:h-[18px] flex-shrink-0" />
90
+ <span className="text-xs sm:text-sm font-medium text-[#2c2c2c] truncate">About This Application</span>
91
  </div>
92
  </div>
93
  </div>
94
 
95
  {/* Content */}
96
+ <div className="p-3 sm:p-6 space-y-3 sm:space-y-5 bg-white rounded-b-lg overflow-y-auto flex-1">
97
  {/* Creator Info */}
98
+ <div className="flex items-start gap-2 sm:gap-4 p-3 sm:p-4 bg-gradient-to-r from-[#E95420]/10 to-orange-100 rounded-lg border border-[#E95420]/20">
99
+ <div className="w-8 h-8 sm:w-10 sm:h-10 rounded-full bg-gradient-to-br from-[#E95420] to-[#d14818] flex items-center justify-center flex-shrink-0">
100
+ <UserCircle size={20} weight="fill" className="text-white sm:w-6 sm:h-6" />
101
  </div>
102
+ <div className="min-w-0">
103
+ <h3 className="text-sm sm:text-base font-semibold text-[#2c2c2c] mb-0.5 sm:mb-1">Created By</h3>
104
+ <p className="text-[#555] font-medium text-xs sm:text-sm truncate">Reuben Chagas Fernandes</p>
105
  </div>
106
  </div>
107
 
108
  {/* Purpose */}
109
+ <div className="flex items-start gap-2 sm:gap-4 p-3 sm:p-4 bg-blue-50 rounded-lg border border-blue-200">
110
+ <div className="w-8 h-8 sm:w-10 sm:h-10 rounded-full bg-gradient-to-br from-blue-500 to-blue-600 flex items-center justify-center flex-shrink-0">
111
+ <Target size={20} weight="fill" className="text-white sm:w-6 sm:h-6" />
112
  </div>
113
+ <div className="min-w-0">
114
+ <h3 className="text-sm sm:text-base font-semibold text-[#2c2c2c] mb-1 sm:mb-2">Purpose</h3>
115
+ <p className="text-[#555] leading-relaxed text-xs sm:text-sm">
116
  This application was created for sharing study material and making it easily
117
  accessible through Claude. Our goal is to provide a seamless platform for
118
  students to collaborate, share resources, and enhance their learning experience.
 
121
  </div>
122
 
123
  {/* Features */}
124
+ <div className="bg-gradient-to-br from-purple-50 to-pink-50 rounded-lg p-3 sm:p-4 border border-purple-200">
125
+ <h4 className="text-xs sm:text-sm font-semibold text-[#2c2c2c] uppercase tracking-wider mb-2 sm:mb-3 flex items-center gap-2">
126
+ <span className="w-1 h-3 sm:h-4 bg-[#E95420] rounded-full"></span>
127
  Key Features
128
  </h4>
129
+ <ul className="space-y-1.5 sm:space-y-2.5 text-[#555]">
130
+ <li className="flex items-center gap-2 sm:gap-3">
131
+ <span className="w-1.5 h-1.5 sm:w-2 sm:h-2 bg-blue-500 rounded-full flex-shrink-0" />
132
+ <span className="text-xs sm:text-sm">Easy file upload and sharing</span>
133
  </li>
134
+ <li className="flex items-center gap-2 sm:gap-3">
135
+ <span className="w-1.5 h-1.5 sm:w-2 sm:h-2 bg-purple-500 rounded-full flex-shrink-0" />
136
+ <span className="text-xs sm:text-sm">Integration with Claude AI</span>
137
  </li>
138
+ <li className="flex items-center gap-2 sm:gap-3">
139
+ <span className="w-1.5 h-1.5 sm:w-2 sm:h-2 bg-green-500 rounded-full flex-shrink-0" />
140
+ <span className="text-xs sm:text-sm">Public folder for community sharing</span>
141
  </li>
142
+ <li className="flex items-center gap-2 sm:gap-3">
143
+ <span className="w-1.5 h-1.5 sm:w-2 sm:h-2 bg-[#E95420] rounded-full flex-shrink-0" />
144
+ <span className="text-xs sm:text-sm">Exam calendar and organization tools</span>
145
  </li>
146
+ <li className="flex items-center gap-2 sm:gap-3">
147
+ <span className="w-1.5 h-1.5 sm:w-2 sm:h-2 bg-cyan-500 rounded-full flex-shrink-0" />
148
+ <span className="text-xs sm:text-sm">Web browser with CORS proxy support</span>
149
  </li>
150
+ <li className="flex items-center gap-2 sm:gap-3">
151
+ <span className="w-1.5 h-1.5 sm:w-2 sm:h-2 bg-orange-500 rounded-full flex-shrink-0" />
152
+ <span className="text-xs sm:text-sm">Gemini AI chat assistant</span>
153
  </li>
154
  </ul>
155
  </div>
156
 
157
  {/* Footer Button */}
158
+ <div className="pt-1 sm:pt-2">
159
  <button
160
  onClick={onClose}
161
+ className="w-full py-2 sm:py-2.5 bg-[#E95420] hover:bg-[#d14818] text-white rounded-lg text-sm sm:text-base font-medium transition-colors shadow-sm"
162
  >
163
  Close
164
  </button>
app/components/Messages.tsx CHANGED
@@ -2,7 +2,7 @@
2
 
3
  import React, { useState, useEffect, useRef } from 'react'
4
  import { motion, AnimatePresence } from 'framer-motion'
5
- import { PaperPlaneRight, MagnifyingGlass, UserCircle, WarningCircle } from '@phosphor-icons/react'
6
  import ReactMarkdown from 'react-markdown'
7
  import remarkGfm from 'remark-gfm'
8
  import rehypeHighlight from 'rehype-highlight'
@@ -31,6 +31,7 @@ export function Messages({ onClose, onMinimize, onMaximize, onFocus, zIndex }: M
31
  const [isLoading, setIsLoading] = useState(false)
32
  const [error, setError] = useState<string | null>(null)
33
  const messagesEndRef = useRef<HTMLDivElement>(null)
 
34
 
35
  // Persistent user identity
36
  const [userId] = useKV<string>('messages-user-id', `user-${Math.random().toString(36).substring(2, 9)}`)
@@ -120,77 +121,114 @@ export function Messages({ onClose, onMinimize, onMaximize, onFocus, zIndex }: M
120
  contentClassName="!bg-transparent"
121
  headerClassName="!bg-transparent border-b border-white/5"
122
  >
123
- <div className="flex h-full text-white overflow-hidden">
124
- {/* Sidebar */}
125
- <div className="w-64 border-r border-white/10 bg-black/20 flex flex-col">
126
- <div className="p-4 border-b border-white/5">
127
- <div className="relative">
128
- <MagnifyingGlass className="absolute left-3 top-1/2 -translate-y-1/2 text-gray-500" size={14} />
129
- <input
130
- type="text"
131
- placeholder="Search"
132
- className="w-full bg-white/10 border-none rounded-md py-1.5 pl-9 pr-3 text-xs text-white placeholder-gray-500 focus:ring-1 focus:ring-blue-500 outline-none"
133
- />
134
- </div>
135
- </div>
136
 
137
- <div className="flex-1 overflow-y-auto p-2 space-y-1">
138
- <div className="p-3 rounded-lg bg-blue-600/20 border border-blue-500/30 cursor-pointer">
139
- <div className="flex justify-between items-start">
140
- <span className="font-semibold text-sm">Global Chat</span>
141
- <span className="text-[10px] text-gray-400">Now</span>
142
- </div>
143
- <div className="text-xs text-gray-400 mt-1 truncate">
144
- {messages.length > 0 ? messages[messages.length - 1].text.replace(/[#*`_~\[\]]/g, '') : 'No messages yet'}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
145
  </div>
146
- </div>
147
- </div>
148
 
149
- {/* User Profile */}
150
- <div className="p-4 border-t border-white/10 bg-black/10">
151
- {isEditingName ? (
152
- <div className="flex gap-2">
153
- <input
154
- type="text"
155
- value={tempName}
156
- onChange={(e) => setTempName(e.target.value)}
157
- className="flex-1 bg-white/10 rounded px-2 py-1 text-sm outline-none border border-blue-500"
158
- autoFocus
159
- onKeyDown={(e) => e.key === 'Enter' && handleNameSave()}
160
- />
161
- <button onClick={handleNameSave} className="text-xs text-blue-400 font-medium">Save</button>
162
- </div>
163
- ) : (
164
- <div className="flex items-center gap-3 cursor-pointer hover:bg-white/5 p-2 rounded-md transition-colors" onClick={() => {
165
- setTempName(userName)
166
- setIsEditingName(true)
167
- }}>
168
- <UserCircle size={32} className="text-gray-400" />
169
- <div className="flex-1 min-w-0">
170
- <div className="text-sm font-medium truncate">{userName}</div>
171
- <div className="text-[10px] text-gray-500">Click to change name</div>
172
  </div>
173
  </div>
174
- )}
175
- </div>
176
- </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
177
 
178
  {/* Chat Area */}
179
- <div className="flex-1 flex flex-col bg-transparent relative">
180
  {/* Header */}
181
- <div className="h-14 border-b border-white/5 flex items-center px-6 bg-[#252525]/30 backdrop-blur-sm justify-between">
182
- <div className="flex flex-col">
183
- <span className="text-sm font-semibold">To: Everyone</span>
184
- <span className="text-[10px] text-gray-400">Global Channel</span>
 
 
 
 
 
 
 
 
185
  </div>
186
- <div className="flex items-center gap-2 text-orange-400 bg-orange-400/10 px-3 py-1 rounded-full border border-orange-400/20">
187
- <WarningCircle size={14} />
188
- <span className="text-[10px] font-medium">Public Chat - Do not share confidential info</span>
189
  </div>
190
  </div>
191
 
192
  {/* Messages List */}
193
- <div className="flex-1 overflow-y-auto p-6 space-y-1 [&::-webkit-scrollbar-thumb]:bg-white/20 [&::-webkit-scrollbar-track]:bg-transparent [&::-webkit-scrollbar]:w-2">
194
  {messages.map((msg, index) => {
195
  const isMe = msg.userId === userId
196
  const showHeader = index === 0 || messages[index - 1].userId !== msg.userId
@@ -199,8 +237,8 @@ export function Messages({ onClose, onMinimize, onMaximize, onFocus, zIndex }: M
199
  return (
200
  <React.Fragment key={msg.id}>
201
  {showTimestamp && (
202
- <div className="text-center my-4">
203
- <span className="text-[10px] text-gray-500 font-medium">
204
  {new Date(msg.timestamp).toLocaleTimeString([], { hour: 'numeric', minute: '2-digit' })}
205
  </span>
206
  </div>
@@ -211,11 +249,11 @@ export function Messages({ onClose, onMinimize, onMaximize, onFocus, zIndex }: M
211
  className={`flex flex-col ${isMe ? 'items-end' : 'items-start'} ${showHeader ? 'mt-2' : 'mt-0.5'}`}
212
  >
213
  {showHeader && !isMe && (
214
- <span className="text-[10px] text-gray-500 ml-3 mb-0.5">{msg.sender}</span>
215
  )}
216
  <div
217
  className={`
218
- max-w-[70%] px-3 py-1.5 text-[13px] leading-relaxed break-words relative group select-text
219
  ${isMe
220
  ? 'bg-[#0A84FF] text-white rounded-2xl rounded-br-sm message-bubble-sent'
221
  : 'bg-[#3a3a3a] text-gray-100 rounded-2xl rounded-bl-sm message-bubble-received'}
@@ -240,7 +278,7 @@ export function Messages({ onClose, onMinimize, onMaximize, onFocus, zIndex }: M
240
  </div>
241
  {/* Delivered status for me */}
242
  {isMe && index === messages.length - 1 && (
243
- <span className="text-[10px] text-gray-500 mr-1 mt-0.5 font-medium">Delivered</span>
244
  )}
245
  </motion.div>
246
  </React.Fragment>
@@ -250,19 +288,19 @@ export function Messages({ onClose, onMinimize, onMaximize, onFocus, zIndex }: M
250
  </div>
251
 
252
  {/* Input Area */}
253
- <div className="p-4 bg-[#1e1e1e]/50 border-t border-white/10 backdrop-blur-md">
254
  {error && (
255
  <motion.div
256
  initial={{ opacity: 0, y: 10 }}
257
  animate={{ opacity: 1, y: 0 }}
258
  exit={{ opacity: 0 }}
259
- className="flex items-center gap-2 text-red-400 text-xs mb-2 px-2 justify-center"
260
  >
261
- <WarningCircle size={14} />
262
  {error}
263
  </motion.div>
264
  )}
265
- <form onSubmit={handleSend} className="relative flex items-center gap-3 max-w-3xl mx-auto w-full">
266
  <div className="flex-1 relative">
267
  <input
268
  type="text"
@@ -281,22 +319,22 @@ export function Messages({ onClose, onMinimize, onMaximize, onFocus, zIndex }: M
281
  placeholder="iMessage"
282
  maxLength={200}
283
  disabled={isLoading}
284
- className="w-full bg-[#2a2a2a] border border-white/10 rounded-full py-1.5 pl-4 pr-10 text-sm text-white placeholder-gray-500 focus:outline-none focus:border-gray-500 transition-colors select-text"
285
  />
286
  <button
287
  type="submit"
288
  disabled={!inputText.trim() || isLoading}
289
  className={`
290
- absolute right-1 top-1/2 -translate-y-1/2 p-1 rounded-full transition-all
291
  ${inputText.trim() && !isLoading ? 'bg-[#0A84FF] text-white scale-100' : 'bg-gray-600 text-gray-400 scale-90 opacity-0 pointer-events-none'}
292
  `}
293
  >
294
- <PaperPlaneRight size={14} weight="fill" />
295
  </button>
296
  </div>
297
  </form>
298
- <div className="text-[9px] text-gray-600 text-center mt-2 font-medium">
299
- {inputText.length}/200 β€’ Press Enter to send
300
  </div>
301
  </div>
302
  </div> </div>
 
2
 
3
  import React, { useState, useEffect, useRef } from 'react'
4
  import { motion, AnimatePresence } from 'framer-motion'
5
+ import { PaperPlaneRight, MagnifyingGlass, UserCircle, WarningCircle, List, X } from '@phosphor-icons/react'
6
  import ReactMarkdown from 'react-markdown'
7
  import remarkGfm from 'remark-gfm'
8
  import rehypeHighlight from 'rehype-highlight'
 
31
  const [isLoading, setIsLoading] = useState(false)
32
  const [error, setError] = useState<string | null>(null)
33
  const messagesEndRef = useRef<HTMLDivElement>(null)
34
+ const [sidebarOpen, setSidebarOpen] = useState(false)
35
 
36
  // Persistent user identity
37
  const [userId] = useKV<string>('messages-user-id', `user-${Math.random().toString(36).substring(2, 9)}`)
 
121
  contentClassName="!bg-transparent"
122
  headerClassName="!bg-transparent border-b border-white/5"
123
  >
124
+ <div className="flex h-full text-white overflow-hidden relative">
125
+ {/* Mobile Sidebar Overlay */}
126
+ <AnimatePresence>
127
+ {sidebarOpen && (
128
+ <motion.div
129
+ initial={{ opacity: 0 }}
130
+ animate={{ opacity: 1 }}
131
+ exit={{ opacity: 0 }}
132
+ className="absolute inset-0 bg-black/50 z-20 md:hidden"
133
+ onClick={() => setSidebarOpen(false)}
134
+ />
135
+ )}
136
+ </AnimatePresence>
137
 
138
+ {/* Sidebar */}
139
+ <AnimatePresence>
140
+ {(sidebarOpen || typeof window !== 'undefined' && window.innerWidth >= 768) && (
141
+ <motion.div
142
+ initial={{ x: -256 }}
143
+ animate={{ x: 0 }}
144
+ exit={{ x: -256 }}
145
+ transition={{ type: 'spring', damping: 25, stiffness: 300 }}
146
+ className={`${sidebarOpen ? 'absolute z-30 h-full' : 'hidden'} md:relative md:flex w-56 sm:w-64 border-r border-white/10 bg-black/90 md:bg-black/20 flex-col`}
147
+ >
148
+ <div className="p-3 sm:p-4 border-b border-white/5 flex items-center justify-between">
149
+ <div className="relative flex-1">
150
+ <MagnifyingGlass className="absolute left-3 top-1/2 -translate-y-1/2 text-gray-500" size={14} />
151
+ <input
152
+ type="text"
153
+ placeholder="Search"
154
+ className="w-full bg-white/10 border-none rounded-md py-1.5 pl-9 pr-3 text-xs text-white placeholder-gray-500 focus:ring-1 focus:ring-blue-500 outline-none"
155
+ />
156
+ </div>
157
+ <button
158
+ onClick={() => setSidebarOpen(false)}
159
+ className="ml-2 p-1 hover:bg-white/10 rounded md:hidden"
160
+ >
161
+ <X size={18} />
162
+ </button>
163
  </div>
 
 
164
 
165
+ <div className="flex-1 overflow-y-auto p-2 space-y-1">
166
+ <div className="p-2 sm:p-3 rounded-lg bg-blue-600/20 border border-blue-500/30 cursor-pointer">
167
+ <div className="flex justify-between items-start">
168
+ <span className="font-semibold text-xs sm:text-sm">Global Chat</span>
169
+ <span className="text-[9px] sm:text-[10px] text-gray-400">Now</span>
170
+ </div>
171
+ <div className="text-[10px] sm:text-xs text-gray-400 mt-1 truncate">
172
+ {messages.length > 0 ? messages[messages.length - 1].text.replace(/[#*`_~\[\]]/g, '') : 'No messages yet'}
173
+ </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
174
  </div>
175
  </div>
176
+
177
+ {/* User Profile */}
178
+ <div className="p-3 sm:p-4 border-t border-white/10 bg-black/10">
179
+ {isEditingName ? (
180
+ <div className="flex gap-2">
181
+ <input
182
+ type="text"
183
+ value={tempName}
184
+ onChange={(e) => setTempName(e.target.value)}
185
+ className="flex-1 bg-white/10 rounded px-2 py-1 text-sm outline-none border border-blue-500 min-w-0"
186
+ autoFocus
187
+ onKeyDown={(e) => e.key === 'Enter' && handleNameSave()}
188
+ />
189
+ <button onClick={handleNameSave} className="text-xs text-blue-400 font-medium flex-shrink-0">Save</button>
190
+ </div>
191
+ ) : (
192
+ <div className="flex items-center gap-2 sm:gap-3 cursor-pointer hover:bg-white/5 p-1.5 sm:p-2 rounded-md transition-colors" onClick={() => {
193
+ setTempName(userName)
194
+ setIsEditingName(true)
195
+ }}>
196
+ <UserCircle size={28} className="text-gray-400 sm:w-8 sm:h-8 flex-shrink-0" />
197
+ <div className="flex-1 min-w-0">
198
+ <div className="text-xs sm:text-sm font-medium truncate">{userName}</div>
199
+ <div className="text-[9px] sm:text-[10px] text-gray-500">Click to change name</div>
200
+ </div>
201
+ </div>
202
+ )}
203
+ </div>
204
+ </motion.div>
205
+ )}
206
+ </AnimatePresence>
207
 
208
  {/* Chat Area */}
209
+ <div className="flex-1 flex flex-col bg-transparent relative min-w-0">
210
  {/* Header */}
211
+ <div className="h-12 sm:h-14 border-b border-white/5 flex items-center px-3 sm:px-6 bg-[#252525]/30 backdrop-blur-sm justify-between gap-2">
212
+ <div className="flex items-center gap-2 sm:gap-3 min-w-0">
213
+ <button
214
+ onClick={() => setSidebarOpen(true)}
215
+ className="p-1.5 hover:bg-white/10 rounded md:hidden flex-shrink-0"
216
+ >
217
+ <List size={18} />
218
+ </button>
219
+ <div className="flex flex-col min-w-0">
220
+ <span className="text-xs sm:text-sm font-semibold truncate">To: Everyone</span>
221
+ <span className="text-[9px] sm:text-[10px] text-gray-400">Global Channel</span>
222
+ </div>
223
  </div>
224
+ <div className="hidden xs:flex items-center gap-1 sm:gap-2 text-orange-400 bg-orange-400/10 px-2 sm:px-3 py-1 rounded-full border border-orange-400/20 flex-shrink-0">
225
+ <WarningCircle size={12} className="sm:w-3.5 sm:h-3.5" />
226
+ <span className="text-[8px] sm:text-[10px] font-medium whitespace-nowrap">Public Chat</span>
227
  </div>
228
  </div>
229
 
230
  {/* Messages List */}
231
+ <div className="flex-1 overflow-y-auto p-3 sm:p-6 space-y-1 [&::-webkit-scrollbar-thumb]:bg-white/20 [&::-webkit-scrollbar-track]:bg-transparent [&::-webkit-scrollbar]:w-2">
232
  {messages.map((msg, index) => {
233
  const isMe = msg.userId === userId
234
  const showHeader = index === 0 || messages[index - 1].userId !== msg.userId
 
237
  return (
238
  <React.Fragment key={msg.id}>
239
  {showTimestamp && (
240
+ <div className="text-center my-3 sm:my-4">
241
+ <span className="text-[9px] sm:text-[10px] text-gray-500 font-medium">
242
  {new Date(msg.timestamp).toLocaleTimeString([], { hour: 'numeric', minute: '2-digit' })}
243
  </span>
244
  </div>
 
249
  className={`flex flex-col ${isMe ? 'items-end' : 'items-start'} ${showHeader ? 'mt-2' : 'mt-0.5'}`}
250
  >
251
  {showHeader && !isMe && (
252
+ <span className="text-[9px] sm:text-[10px] text-gray-500 ml-2 sm:ml-3 mb-0.5">{msg.sender}</span>
253
  )}
254
  <div
255
  className={`
256
+ max-w-[85%] sm:max-w-[70%] px-2.5 sm:px-3 py-1.5 text-xs sm:text-[13px] leading-relaxed break-words relative group select-text
257
  ${isMe
258
  ? 'bg-[#0A84FF] text-white rounded-2xl rounded-br-sm message-bubble-sent'
259
  : 'bg-[#3a3a3a] text-gray-100 rounded-2xl rounded-bl-sm message-bubble-received'}
 
278
  </div>
279
  {/* Delivered status for me */}
280
  {isMe && index === messages.length - 1 && (
281
+ <span className="text-[9px] sm:text-[10px] text-gray-500 mr-1 mt-0.5 font-medium">Delivered</span>
282
  )}
283
  </motion.div>
284
  </React.Fragment>
 
288
  </div>
289
 
290
  {/* Input Area */}
291
+ <div className="p-2 sm:p-4 bg-[#1e1e1e]/50 border-t border-white/10 backdrop-blur-md">
292
  {error && (
293
  <motion.div
294
  initial={{ opacity: 0, y: 10 }}
295
  animate={{ opacity: 1, y: 0 }}
296
  exit={{ opacity: 0 }}
297
+ className="flex items-center gap-2 text-red-400 text-[10px] sm:text-xs mb-2 px-2 justify-center"
298
  >
299
+ <WarningCircle size={12} className="sm:w-3.5 sm:h-3.5" />
300
  {error}
301
  </motion.div>
302
  )}
303
+ <form onSubmit={handleSend} className="relative flex items-center gap-2 sm:gap-3 max-w-3xl mx-auto w-full">
304
  <div className="flex-1 relative">
305
  <input
306
  type="text"
 
319
  placeholder="iMessage"
320
  maxLength={200}
321
  disabled={isLoading}
322
+ className="w-full bg-[#2a2a2a] border border-white/10 rounded-full py-1.5 sm:py-2 pl-3 sm:pl-4 pr-9 sm:pr-10 text-xs sm:text-sm text-white placeholder-gray-500 focus:outline-none focus:border-gray-500 transition-colors select-text"
323
  />
324
  <button
325
  type="submit"
326
  disabled={!inputText.trim() || isLoading}
327
  className={`
328
+ absolute right-1 top-1/2 -translate-y-1/2 p-1 sm:p-1.5 rounded-full transition-all
329
  ${inputText.trim() && !isLoading ? 'bg-[#0A84FF] text-white scale-100' : 'bg-gray-600 text-gray-400 scale-90 opacity-0 pointer-events-none'}
330
  `}
331
  >
332
+ <PaperPlaneRight size={12} className="sm:w-3.5 sm:h-3.5" weight="fill" />
333
  </button>
334
  </div>
335
  </form>
336
+ <div className="text-[8px] sm:text-[9px] text-gray-600 text-center mt-1 sm:mt-2 font-medium">
337
+ {inputText.length}/200 β€’ Enter to send
338
  </div>
339
  </div>
340
  </div> </div>
app/components/SpotlightSearch.tsx CHANGED
@@ -114,50 +114,50 @@ export function SpotlightSearch({ isOpen, onClose, onOpenApp }: SpotlightSearchP
114
  animate={{ opacity: 1, scale: 1, y: 0 }}
115
  exit={{ opacity: 0, scale: 0.95, y: -20 }}
116
  transition={{ duration: 0.15, ease: 'easeOut' }}
117
- className="fixed top-[20%] left-1/2 -translate-x-1/2 w-[600px] max-w-[90%] glass rounded-xl shadow-2xl z-[9999]"
118
  >
119
- <div className="flex items-center px-4 py-3 gap-3 border-b border-gray-200/20">
120
- <MagnifyingGlass size={24} weight="regular" className="text-gray-600" />
121
  <input
122
  ref={inputRef}
123
  type="text"
124
  value={query}
125
  onChange={(e) => setQuery(e.target.value)}
126
  onKeyDown={handleKeyDown}
127
- className="flex-1 bg-transparent text-xl focus:outline-none text-gray-800 placeholder-gray-400"
128
  placeholder="Spotlight Search"
129
  autoComplete="off"
130
  spellCheck={false}
131
  />
132
  <button
133
  onClick={onClose}
134
- className="text-gray-500 hover:text-gray-700 transition-colors"
135
  >
136
- <X size={20} weight="regular" />
137
  </button>
138
  </div>
139
 
140
  {/* Results */}
141
  {results.length > 0 && (
142
- <div className="max-h-96 overflow-y-auto p-2">
143
  {results.map((result, index) => (
144
  <div
145
  key={result.id}
146
  onClick={() => handleSelect(result)}
147
- className={`flex items-center px-3 py-2.5 hover:bg-blue-500 hover:text-white rounded-lg cursor-pointer transition-colors gap-3 ${index === 0 ? 'bg-blue-500/10' : ''
148
  }`}
149
  >
150
  {result.icon && (
151
- <div className="w-8 h-8 flex items-center justify-center">
152
  {result.icon}
153
  </div>
154
  )}
155
- <div className="flex-1 flex flex-col">
156
- <span className="font-medium">{result.name}</span>
157
- <span className="text-xs opacity-70 capitalize">{result.type}</span>
158
  </div>
159
  {result.type === 'app' && (
160
- <span className="text-xs opacity-50">⌘ Enter</span>
161
  )}
162
  </div>
163
  ))}
@@ -166,25 +166,25 @@ export function SpotlightSearch({ isOpen, onClose, onOpenApp }: SpotlightSearchP
166
 
167
  {/* No results */}
168
  {query.trim() !== '' && results.length === 0 && (
169
- <div className="p-4 text-center text-gray-500">
170
  No results found for "{query}"
171
  </div>
172
  )}
173
 
174
  {/* Quick Actions (when no query) */}
175
  {query === '' && (
176
- <div className="p-4">
177
- <div className="text-xs font-semibold text-gray-500 uppercase tracking-wider mb-2">
178
  Suggestions
179
  </div>
180
- <div className="grid grid-cols-2 gap-2">
181
  {apps.slice(0, 4).map(app => (
182
  <div
183
  key={app.id}
184
  onClick={() => handleSelect(app)}
185
- className="flex items-center px-3 py-2 hover:bg-gray-100 rounded-lg cursor-pointer transition-colors"
186
  >
187
- <span className="text-sm">{app.name}</span>
188
  </div>
189
  ))}
190
  </div>
 
114
  animate={{ opacity: 1, scale: 1, y: 0 }}
115
  exit={{ opacity: 0, scale: 0.95, y: -20 }}
116
  transition={{ duration: 0.15, ease: 'easeOut' }}
117
+ className="fixed top-[15%] sm:top-[20%] left-1/2 -translate-x-1/2 w-[95vw] sm:w-[600px] max-w-[600px] glass rounded-xl shadow-2xl z-[9999]"
118
  >
119
+ <div className="flex items-center px-3 sm:px-4 py-2.5 sm:py-3 gap-2 sm:gap-3 border-b border-gray-200/20">
120
+ <MagnifyingGlass size={20} weight="regular" className="text-gray-600 sm:w-6 sm:h-6 flex-shrink-0" />
121
  <input
122
  ref={inputRef}
123
  type="text"
124
  value={query}
125
  onChange={(e) => setQuery(e.target.value)}
126
  onKeyDown={handleKeyDown}
127
+ className="flex-1 bg-transparent text-base sm:text-xl focus:outline-none text-gray-800 placeholder-gray-400 min-w-0"
128
  placeholder="Spotlight Search"
129
  autoComplete="off"
130
  spellCheck={false}
131
  />
132
  <button
133
  onClick={onClose}
134
+ className="text-gray-500 hover:text-gray-700 transition-colors flex-shrink-0"
135
  >
136
+ <X size={18} weight="regular" className="sm:w-5 sm:h-5" />
137
  </button>
138
  </div>
139
 
140
  {/* Results */}
141
  {results.length > 0 && (
142
+ <div className="max-h-64 sm:max-h-96 overflow-y-auto p-1.5 sm:p-2">
143
  {results.map((result, index) => (
144
  <div
145
  key={result.id}
146
  onClick={() => handleSelect(result)}
147
+ className={`flex items-center px-2 sm:px-3 py-2 sm:py-2.5 hover:bg-blue-500 hover:text-white rounded-lg cursor-pointer transition-colors gap-2 sm:gap-3 ${index === 0 ? 'bg-blue-500/10' : ''
148
  }`}
149
  >
150
  {result.icon && (
151
+ <div className="w-7 h-7 sm:w-8 sm:h-8 flex items-center justify-center flex-shrink-0">
152
  {result.icon}
153
  </div>
154
  )}
155
+ <div className="flex-1 flex flex-col min-w-0">
156
+ <span className="font-medium text-sm sm:text-base truncate">{result.name}</span>
157
+ <span className="text-[10px] sm:text-xs opacity-70 capitalize">{result.type}</span>
158
  </div>
159
  {result.type === 'app' && (
160
+ <span className="text-[10px] sm:text-xs opacity-50 hidden xs:inline flex-shrink-0">⌘ Enter</span>
161
  )}
162
  </div>
163
  ))}
 
166
 
167
  {/* No results */}
168
  {query.trim() !== '' && results.length === 0 && (
169
+ <div className="p-3 sm:p-4 text-center text-gray-500 text-sm sm:text-base">
170
  No results found for "{query}"
171
  </div>
172
  )}
173
 
174
  {/* Quick Actions (when no query) */}
175
  {query === '' && (
176
+ <div className="p-3 sm:p-4">
177
+ <div className="text-[10px] sm:text-xs font-semibold text-gray-500 uppercase tracking-wider mb-2">
178
  Suggestions
179
  </div>
180
+ <div className="grid grid-cols-2 gap-1.5 sm:gap-2">
181
  {apps.slice(0, 4).map(app => (
182
  <div
183
  key={app.id}
184
  onClick={() => handleSelect(app)}
185
+ className="flex items-center px-2 sm:px-3 py-1.5 sm:py-2 hover:bg-gray-100 rounded-lg cursor-pointer transition-colors"
186
  >
187
+ <span className="text-xs sm:text-sm truncate">{app.name}</span>
188
  </div>
189
  ))}
190
  </div>
app/components/TextEditor.tsx CHANGED
@@ -226,58 +226,60 @@ export function TextEditor({
226
  >
227
  <div className="flex flex-col h-full bg-[#1e1e1e]">
228
  {/* Toolbar */}
229
- <div className="h-12 bg-[#2d2d2d] border-b border-[#1e1e1e] flex items-center justify-between px-4">
230
- <div className="flex items-center gap-3">
231
- <div className="flex items-center gap-2 px-3 py-1.5 bg-[#1e1e1e] rounded text-xs text-gray-400 border border-[#333]">
232
- <FileText size={14} className="text-blue-400" />
233
- <span>{fileName}</span>
234
  </div>
235
 
236
  {hasChanges && (
237
- <span className="text-xs text-yellow-500">● Modified</span>
238
  )}
239
 
240
  {saveStatus === 'saved' && (
241
- <div className="flex items-center gap-1 text-xs text-green-500">
242
- <Check size={14} />
243
- <span>Saved {lastSaved?.toLocaleTimeString()}</span>
 
244
  </div>
245
  )}
246
 
247
  {saveStatus === 'error' && (
248
- <div className="flex items-center gap-1 text-xs text-red-500">
249
- <WarningCircle size={14} />
250
- <span>Save failed</span>
251
  </div>
252
  )}
253
  </div>
254
 
255
- <div className="flex items-center gap-2">
256
  <button
257
  onClick={handleDownload}
258
- className="flex items-center gap-2 px-3 py-1.5 bg-gray-700 hover:bg-gray-600 text-white rounded text-xs font-medium transition-colors"
259
  title="Download file"
260
  >
261
- <svg width="14" height="14" viewBox="0 0 256 256" fill="currentColor">
262
  <path d="M224,144v64a8,8,0,0,1-8,8H40a8,8,0,0,1-8-8V144a8,8,0,0,1,16,0v56H208V144a8,8,0,0,1,16,0Zm-101.66,5.66a8,8,0,0,0,11.32,0l40-40a8,8,0,0,0-11.32-11.32L136,124.69V40a8,8,0,0,0-16,0v84.69L93.66,98.34a8,8,0,0,0-11.32,11.32Z"></path>
263
  </svg>
264
- <span>Download</span>
265
  </button>
266
- <div className="h-4 w-[1px] bg-gray-600 mx-1" />
267
  <button
268
  onClick={handleSave}
269
  disabled={isSaving || !hasChanges}
270
- className="flex items-center gap-2 px-4 py-1.5 bg-blue-600 hover:bg-blue-700 disabled:bg-gray-600 disabled:cursor-not-allowed text-white rounded text-xs font-medium transition-colors"
271
  >
272
  {isSaving ? (
273
  <>
274
- <div className="w-3 h-3 border-2 border-white border-t-transparent rounded-full animate-spin" />
275
- <span>Saving...</span>
276
  </>
277
  ) : (
278
  <>
279
- <FloppyDisk size={14} weight="fill" />
280
- <span>Save (⌘S)</span>
 
281
  </>
282
  )}
283
  </button>
@@ -293,13 +295,13 @@ export function TextEditor({
293
  value={code}
294
  onChange={(value) => setCode(value || '')}
295
  options={{
296
- minimap: { enabled: true },
297
- fontSize: 14,
298
  fontFamily: "'JetBrains Mono', 'Fira Code', 'Cascadia Code', monospace",
299
  lineNumbers: 'on',
300
  scrollBeyondLastLine: false,
301
  automaticLayout: true,
302
- padding: { top: 16, bottom: 16 },
303
  renderLineHighlight: 'all',
304
  smoothScrolling: true,
305
  cursorBlinking: 'smooth',
@@ -313,16 +315,16 @@ export function TextEditor({
313
  </div>
314
 
315
  {/* Status Bar */}
316
- <div className="h-6 bg-[#007acc] flex items-center justify-between px-4 text-xs text-white">
317
- <div className="flex items-center gap-4">
318
  <span>{language.toUpperCase()}</span>
319
- <span>|</span>
320
- <span>{code.split('\n').length} lines</span>
321
- <span>|</span>
322
- <span>{code.length} characters</span>
323
  </div>
324
- <div>
325
- {isLatexFile && <span>πŸ’‘ Download and compile .tex file locally with LaTeX</span>}
326
  </div>
327
  </div>
328
  </div>
 
226
  >
227
  <div className="flex flex-col h-full bg-[#1e1e1e]">
228
  {/* Toolbar */}
229
+ <div className="h-10 sm:h-12 bg-[#2d2d2d] border-b border-[#1e1e1e] flex items-center justify-between px-2 sm:px-4 gap-2">
230
+ <div className="flex items-center gap-1.5 sm:gap-3 min-w-0 flex-1">
231
+ <div className="flex items-center gap-1.5 sm:gap-2 px-2 sm:px-3 py-1 sm:py-1.5 bg-[#1e1e1e] rounded text-[10px] sm:text-xs text-gray-400 border border-[#333] min-w-0">
232
+ <FileText size={12} className="text-blue-400 sm:w-3.5 sm:h-3.5 flex-shrink-0" />
233
+ <span className="truncate max-w-[60px] xs:max-w-[100px] sm:max-w-none">{fileName}</span>
234
  </div>
235
 
236
  {hasChanges && (
237
+ <span className="text-[10px] sm:text-xs text-yellow-500 flex-shrink-0">● <span className="hidden xs:inline">Modified</span></span>
238
  )}
239
 
240
  {saveStatus === 'saved' && (
241
+ <div className="hidden xs:flex items-center gap-1 text-[10px] sm:text-xs text-green-500">
242
+ <Check size={12} className="sm:w-3.5 sm:h-3.5" />
243
+ <span className="hidden sm:inline">Saved {lastSaved?.toLocaleTimeString()}</span>
244
+ <span className="sm:hidden">Saved</span>
245
  </div>
246
  )}
247
 
248
  {saveStatus === 'error' && (
249
+ <div className="flex items-center gap-1 text-[10px] sm:text-xs text-red-500">
250
+ <WarningCircle size={12} className="sm:w-3.5 sm:h-3.5" />
251
+ <span className="hidden xs:inline">Save failed</span>
252
  </div>
253
  )}
254
  </div>
255
 
256
+ <div className="flex items-center gap-1 sm:gap-2 flex-shrink-0">
257
  <button
258
  onClick={handleDownload}
259
+ className="flex items-center gap-1 sm:gap-2 px-2 sm:px-3 py-1 sm:py-1.5 bg-gray-700 hover:bg-gray-600 text-white rounded text-[10px] sm:text-xs font-medium transition-colors"
260
  title="Download file"
261
  >
262
+ <svg width="12" height="12" viewBox="0 0 256 256" fill="currentColor" className="sm:w-3.5 sm:h-3.5">
263
  <path d="M224,144v64a8,8,0,0,1-8,8H40a8,8,0,0,1-8-8V144a8,8,0,0,1,16,0v56H208V144a8,8,0,0,1,16,0Zm-101.66,5.66a8,8,0,0,0,11.32,0l40-40a8,8,0,0,0-11.32-11.32L136,124.69V40a8,8,0,0,0-16,0v84.69L93.66,98.34a8,8,0,0,0-11.32,11.32Z"></path>
264
  </svg>
265
+ <span className="hidden xs:inline">Download</span>
266
  </button>
267
+ <div className="hidden sm:block h-4 w-[1px] bg-gray-600 mx-1" />
268
  <button
269
  onClick={handleSave}
270
  disabled={isSaving || !hasChanges}
271
+ className="flex items-center gap-1 sm:gap-2 px-2 sm:px-4 py-1 sm:py-1.5 bg-blue-600 hover:bg-blue-700 disabled:bg-gray-600 disabled:cursor-not-allowed text-white rounded text-[10px] sm:text-xs font-medium transition-colors"
272
  >
273
  {isSaving ? (
274
  <>
275
+ <div className="w-2.5 h-2.5 sm:w-3 sm:h-3 border-2 border-white border-t-transparent rounded-full animate-spin" />
276
+ <span className="hidden xs:inline">Saving...</span>
277
  </>
278
  ) : (
279
  <>
280
+ <FloppyDisk size={12} weight="fill" className="sm:w-3.5 sm:h-3.5" />
281
+ <span className="hidden xs:inline">Save</span>
282
+ <span className="hidden sm:inline"> (⌘S)</span>
283
  </>
284
  )}
285
  </button>
 
295
  value={code}
296
  onChange={(value) => setCode(value || '')}
297
  options={{
298
+ minimap: { enabled: typeof window !== 'undefined' && window.innerWidth >= 768 },
299
+ fontSize: typeof window !== 'undefined' && window.innerWidth < 640 ? 12 : 14,
300
  fontFamily: "'JetBrains Mono', 'Fira Code', 'Cascadia Code', monospace",
301
  lineNumbers: 'on',
302
  scrollBeyondLastLine: false,
303
  automaticLayout: true,
304
+ padding: { top: 12, bottom: 12 },
305
  renderLineHighlight: 'all',
306
  smoothScrolling: true,
307
  cursorBlinking: 'smooth',
 
315
  </div>
316
 
317
  {/* Status Bar */}
318
+ <div className="h-5 sm:h-6 bg-[#007acc] flex items-center justify-between px-2 sm:px-4 text-[9px] sm:text-xs text-white">
319
+ <div className="flex items-center gap-2 sm:gap-4">
320
  <span>{language.toUpperCase()}</span>
321
+ <span className="hidden xs:inline">|</span>
322
+ <span className="hidden xs:inline">{code.split('\n').length} lines</span>
323
+ <span className="hidden sm:inline">|</span>
324
+ <span className="hidden sm:inline">{code.length} chars</span>
325
  </div>
326
+ <div className="hidden md:block">
327
+ {isLatexFile && <span>πŸ’‘ Download and compile .tex file locally</span>}
328
  </div>
329
  </div>
330
  </div>
mcp-server.js CHANGED
@@ -547,44 +547,86 @@ class ReubenOSMCPServer {
547
  };
548
  }
549
 
550
- const fullQuizData = {
551
- ...quizData,
 
 
 
 
 
 
 
 
 
 
 
552
  createdAt: new Date().toISOString(),
553
- passkey: passkey,
554
  version: '1.0',
555
  };
556
 
557
- // Quizzes are always saved securely with passkey, never public
558
- const response = await fetch(API_ENDPOINT, {
 
 
 
 
 
 
 
 
 
 
 
 
559
  method: 'POST',
560
  headers: { 'Content-Type': 'application/json' },
561
  body: JSON.stringify({
562
  passkey,
563
  action: 'deploy_quiz',
564
  fileName: 'quiz.json',
565
- content: JSON.stringify(fullQuizData, null, 2),
566
- isPublic: false, // Always secure for quizzes
567
  }),
568
  });
569
 
570
- const data = await response.json();
571
-
572
- if (response.ok && data.success) {
573
- const totalPoints = quizData.questions.reduce((sum, q) => sum + (q.points || 1), 0);
574
 
 
575
  return {
576
- content: [
577
- {
578
- type: 'text',
579
- text: `βœ… Quiz deployed: ${quizData.title}\nπŸ“Š ${quizData.questions.length} questions, ${totalPoints} points\nπŸ”’ Secured with passkey: ${passkey}`,
580
- },
581
- ],
582
- };
583
- } else {
584
- return {
585
- content: [{ type: 'text', text: `❌ Failed: ${data.error || 'Unknown error'}` }],
586
  };
587
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
588
  } catch (error) {
589
  return {
590
  content: [{ type: 'text', text: `❌ Error: ${error.message}` }],
@@ -705,10 +747,12 @@ class ReubenOSMCPServer {
705
  };
706
  }
707
 
708
- // Read quiz.json (quizzes are always secure, never public)
709
  const quizUrl = new URL(API_ENDPOINT);
710
  quizUrl.searchParams.set('passkey', passkey);
711
 
 
 
712
  const quizResponse = await fetch(quizUrl, {
713
  method: 'GET',
714
  headers: { 'Content-Type': 'application/json' },
@@ -716,119 +760,221 @@ class ReubenOSMCPServer {
716
 
717
  const quizData = await quizResponse.json();
718
 
 
 
719
  if (!quizResponse.ok || !quizData.success) {
720
  return {
721
- content: [{ type: 'text', text: `❌ Failed to read quiz.json: ${quizData.error || 'Unknown error'}` }],
 
 
 
 
 
 
722
  };
723
  }
724
 
 
725
  const quizFile = quizData.files.find(f => f.name === 'quiz.json');
726
  if (!quizFile) {
727
  return {
728
- content: [{ type: 'text', text: '❌ quiz.json not found in storage' }],
 
 
 
 
 
 
729
  };
730
  }
731
 
732
- // Read quiz_answers.json
 
 
 
 
 
 
 
 
733
  const answersFile = quizData.files.find(f => f.name === 'quiz_answers.json');
734
  if (!answersFile) {
735
  return {
736
- content: [{ type: 'text', text: '❌ quiz_answers.json not found in storage' }],
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
737
  };
738
  }
739
 
740
- // Parse the quiz and answers
741
- const quiz = JSON.parse(quizFile.content);
742
- const answersData = JSON.parse(answersFile.content);
743
 
744
- // Convert answers array to object for easier lookup
745
- const answers = {};
746
- if (answersData.answers && Array.isArray(answersData.answers)) {
747
  // QuizApp format: { answers: [{ questionId, answer }], metadata: {...} }
748
- answersData.answers.forEach(item => {
749
- answers[item.questionId] = item.answer;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
750
  });
751
- } else {
752
- // Fallback for direct object format
753
- Object.assign(answers, answersData);
754
  }
755
 
 
 
 
756
  // Analyze the answers
757
  let correctCount = 0;
758
  let totalPoints = 0;
759
  let maxPoints = 0;
760
  const feedback = [];
 
 
 
 
 
 
 
761
 
762
- quiz.questions.forEach((question, index) => {
763
- const userAnswer = answers[question.id];
764
- const points = question.points || 1;
 
 
 
765
  maxPoints += points;
766
 
767
- // For multiple choice questions, check if answer matches any correct option
768
- if (question.type === 'multiple_choice') {
769
- const correctAnswer = question.correctAnswer || question.correct;
770
- const isCorrect = userAnswer === correctAnswer ||
771
- (Array.isArray(correctAnswer) && correctAnswer.includes(userAnswer));
772
 
773
- if (isCorrect) {
774
- correctCount++;
775
- totalPoints += points;
776
- feedback.push(`βœ… Question ${index + 1} (${question.id}): Correct! (+${points} points)`);
777
- } else {
778
- feedback.push(`❌ Question ${index + 1} (${question.id}): Incorrect. Your answer: "${userAnswer}"${question.explanation ? `. ${question.explanation}` : ''}`);
779
- }
780
- } else if (question.type === 'true_false') {
781
- const correctAnswer = question.correctAnswer || question.correct;
782
- const isCorrect = String(userAnswer).toLowerCase() === String(correctAnswer).toLowerCase();
783
-
784
- if (isCorrect) {
785
- correctCount++;
786
- totalPoints += points;
787
- feedback.push(`βœ… Question ${index + 1} (${question.id}): Correct! (+${points} points)`);
788
- } else {
789
- feedback.push(`❌ Question ${index + 1} (${question.id}): Incorrect. Your answer: "${userAnswer}"${question.explanation ? `. ${question.explanation}` : ''}`);
790
- }
791
  } else {
792
- // For other question types, do a simple string comparison
793
- const correctAnswer = question.correctAnswer || question.answer || question.correct;
794
- const isCorrect = userAnswer === correctAnswer;
795
-
796
- if (isCorrect) {
797
- correctCount++;
798
- totalPoints += points;
799
- feedback.push(`βœ… Question ${index + 1} (${question.id}): Correct! (+${points} points)`);
800
- } else {
801
- feedback.push(`❌ Question ${index + 1} (${question.id}): Your answer: "${userAnswer}"${question.explanation ? `. ${question.explanation}` : ''}`);
802
- }
803
  }
804
  });
805
 
806
- const percentage = Math.round((totalPoints / maxPoints) * 100);
807
  const grade = percentage >= 90 ? 'A' :
808
  percentage >= 80 ? 'B' :
809
  percentage >= 70 ? 'C' :
810
  percentage >= 60 ? 'D' : 'F';
811
 
 
 
 
 
 
 
 
 
 
 
 
812
  return {
813
  content: [
814
  {
815
  type: 'text',
816
- text: `πŸ“Š Quiz Analysis Results for "${quiz.title}"
817
 
818
  πŸ“ˆ Score: ${totalPoints}/${maxPoints} points (${percentage}%)
819
- βœ… Correct Answers: ${correctCount}/${quiz.questions.length}
820
- 🎯 Grade: ${grade}
821
 
822
- πŸ“ Question Feedback:
823
- ${feedback.join('\n')}
824
 
825
  ${percentage >= 70 ? 'πŸŽ‰ Great job!' : 'πŸ“š Keep studying and try again!'}`,
826
  },
827
  ],
828
  };
829
  } catch (error) {
 
830
  return {
831
- content: [{ type: 'text', text: `❌ Error analyzing quiz: ${error.message}` }],
832
  };
833
  }
834
  }
 
547
  };
548
  }
549
 
550
+ // Strip correct answers from questions before saving quiz.json
551
+ // This prevents users from seeing the answers in the quiz file
552
+ const questionsWithoutAnswers = quizData.questions.map(q => {
553
+ const { correctAnswer, correct, answer, ...questionWithoutAnswer } = q;
554
+ return questionWithoutAnswer;
555
+ });
556
+
557
+ // Save quiz.json WITHOUT correct answers (for users to take)
558
+ const quizForUsers = {
559
+ title: quizData.title,
560
+ description: quizData.description,
561
+ timeLimit: quizData.timeLimit,
562
+ questions: questionsWithoutAnswers,
563
  createdAt: new Date().toISOString(),
 
564
  version: '1.0',
565
  };
566
 
567
+ // Save quiz_key.json WITH correct answers (for grading) - hidden file
568
+ const quizAnswerKey = {
569
+ title: quizData.title,
570
+ questions: quizData.questions.map(q => ({
571
+ id: q.id,
572
+ correctAnswer: q.correctAnswer || q.correct || q.answer,
573
+ explanation: q.explanation,
574
+ points: q.points || 1,
575
+ })),
576
+ createdAt: new Date().toISOString(),
577
+ };
578
+
579
+ // Save quiz.json (without answers)
580
+ const quizResponse = await fetch(API_ENDPOINT, {
581
  method: 'POST',
582
  headers: { 'Content-Type': 'application/json' },
583
  body: JSON.stringify({
584
  passkey,
585
  action: 'deploy_quiz',
586
  fileName: 'quiz.json',
587
+ content: JSON.stringify(quizForUsers, null, 2),
588
+ isPublic: false,
589
  }),
590
  });
591
 
592
+ const quizResult = await quizResponse.json();
 
 
 
593
 
594
+ if (!quizResponse.ok || !quizResult.success) {
595
  return {
596
+ content: [{ type: 'text', text: `❌ Failed to save quiz: ${quizResult.error || 'Unknown error'}` }],
 
 
 
 
 
 
 
 
 
597
  };
598
  }
599
+
600
+ // Save quiz_key.json (with correct answers for grading)
601
+ const keyResponse = await fetch(API_ENDPOINT, {
602
+ method: 'POST',
603
+ headers: { 'Content-Type': 'application/json' },
604
+ body: JSON.stringify({
605
+ passkey,
606
+ action: 'save_file',
607
+ fileName: 'quiz_key.json',
608
+ content: JSON.stringify(quizAnswerKey, null, 2),
609
+ isPublic: false,
610
+ }),
611
+ });
612
+
613
+ const keyResult = await keyResponse.json();
614
+
615
+ if (!keyResponse.ok || !keyResult.success) {
616
+ console.error('Failed to save quiz key:', keyResult.error);
617
+ // Don't fail the whole operation, just log it
618
+ }
619
+
620
+ const totalPoints = quizData.questions.reduce((sum, q) => sum + (q.points || 1), 0);
621
+
622
+ return {
623
+ content: [
624
+ {
625
+ type: 'text',
626
+ text: `βœ… Quiz deployed: ${quizData.title}\nπŸ“Š ${quizData.questions.length} questions, ${totalPoints} points\nπŸ”’ Secured with passkey: ${passkey}\nπŸ“ Answer key saved separately for grading`,
627
+ },
628
+ ],
629
+ };
630
  } catch (error) {
631
  return {
632
  content: [{ type: 'text', text: `❌ Error: ${error.message}` }],
 
747
  };
748
  }
749
 
750
+ // Read all files from secure storage
751
  const quizUrl = new URL(API_ENDPOINT);
752
  quizUrl.searchParams.set('passkey', passkey);
753
 
754
+ console.log('Fetching quiz files from:', quizUrl.toString());
755
+
756
  const quizResponse = await fetch(quizUrl, {
757
  method: 'GET',
758
  headers: { 'Content-Type': 'application/json' },
 
760
 
761
  const quizData = await quizResponse.json();
762
 
763
+ console.log('API Response:', JSON.stringify(quizData, null, 2).substring(0, 500));
764
+
765
  if (!quizResponse.ok || !quizData.success) {
766
  return {
767
+ content: [{ type: 'text', text: `❌ Failed to read files: ${quizData.error || 'Unknown error'}` }],
768
+ };
769
+ }
770
+
771
+ if (!quizData.files || quizData.files.length === 0) {
772
+ return {
773
+ content: [{ type: 'text', text: '❌ No files found for this passkey. Make sure the quiz has been deployed and answered.' }],
774
  };
775
  }
776
 
777
+ // Find quiz.json (questions without answers)
778
  const quizFile = quizData.files.find(f => f.name === 'quiz.json');
779
  if (!quizFile) {
780
  return {
781
+ content: [{ type: 'text', text: `❌ quiz.json not found. Available files: ${quizData.files.map(f => f.name).join(', ')}` }],
782
+ };
783
+ }
784
+
785
+ if (!quizFile.content) {
786
+ return {
787
+ content: [{ type: 'text', text: '❌ quiz.json exists but content is empty or could not be read' }],
788
  };
789
  }
790
 
791
+ // Find quiz_key.json (correct answers for grading)
792
+ // For backward compatibility, if quiz_key.json doesn't exist, try to get answers from quiz.json
793
+ const keyFile = quizData.files.find(f => f.name === 'quiz_key.json');
794
+ const hasAnswerKey = keyFile && keyFile.content;
795
+
796
+ // For backward compatibility: if no quiz_key.json exists, we'll need to get answers from quiz.json (old format)
797
+ // This happens for quizzes created before the answer-key separation was implemented
798
+
799
+ // Find quiz_answers.json (user's answers)
800
  const answersFile = quizData.files.find(f => f.name === 'quiz_answers.json');
801
  if (!answersFile) {
802
  return {
803
+ content: [{ type: 'text', text: `❌ quiz_answers.json not found. The user needs to complete the quiz first. Available files: ${quizData.files.map(f => f.name).join(', ')}` }],
804
+ };
805
+ }
806
+
807
+ if (!answersFile.content) {
808
+ return {
809
+ content: [{ type: 'text', text: '❌ quiz_answers.json exists but content is empty or could not be read' }],
810
+ };
811
+ }
812
+
813
+ console.log('Quiz file content length:', quizFile.content?.length);
814
+ console.log('Key file found:', !!keyFile, 'content length:', keyFile?.content?.length);
815
+ console.log('Answers file content length:', answersFile.content?.length);
816
+
817
+ // Parse the quiz, answer key, and user answers
818
+ let quiz, answerKey, userAnswersData;
819
+ try {
820
+ quiz = typeof quizFile.content === 'string' ? JSON.parse(quizFile.content) : quizFile.content;
821
+ } catch (e) {
822
+ return {
823
+ content: [{ type: 'text', text: `❌ Failed to parse quiz.json: ${e.message}` }],
824
+ };
825
+ }
826
+
827
+ // Parse answer key (from quiz_key.json if exists, or fall back to quiz.json for backward compatibility)
828
+ if (hasAnswerKey) {
829
+ try {
830
+ answerKey = typeof keyFile.content === 'string' ? JSON.parse(keyFile.content) : keyFile.content;
831
+ } catch (e) {
832
+ return {
833
+ content: [{ type: 'text', text: `❌ Failed to parse quiz_key.json: ${e.message}` }],
834
+ };
835
+ }
836
+ } else {
837
+ // Backward compatibility: create answer key from quiz.json (old format had answers in quiz.json)
838
+ console.log('No quiz_key.json found - using backward compatibility mode with quiz.json');
839
+ answerKey = {
840
+ title: quiz.title,
841
+ questions: (quiz.questions || []).map(q => ({
842
+ id: q.id,
843
+ correctAnswer: q.correctAnswer || q.correct || q.answer,
844
+ explanation: q.explanation,
845
+ points: q.points || 1,
846
+ })),
847
+ };
848
+ }
849
+
850
+ try {
851
+ userAnswersData = typeof answersFile.content === 'string' ? JSON.parse(answersFile.content) : answersFile.content;
852
+ } catch (e) {
853
+ return {
854
+ content: [{ type: 'text', text: `❌ Failed to parse quiz_answers.json: ${e.message}` }],
855
  };
856
  }
857
 
858
+ console.log('Parsed quiz:', quiz.title, 'Questions:', quiz.questions?.length);
859
+ console.log('Parsed answer key:', answerKey.questions?.length, 'answers');
860
+ console.log('Parsed user answers:', JSON.stringify(userAnswersData, null, 2).substring(0, 300));
861
 
862
+ // Convert user answers array to object for easier lookup
863
+ const userAnswers = {};
864
+ if (userAnswersData.answers && Array.isArray(userAnswersData.answers)) {
865
  // QuizApp format: { answers: [{ questionId, answer }], metadata: {...} }
866
+ userAnswersData.answers.forEach(item => {
867
+ userAnswers[item.questionId] = item.answer;
868
+ });
869
+ } else if (typeof userAnswersData === 'object') {
870
+ // Direct object format
871
+ Object.assign(userAnswers, userAnswersData);
872
+ }
873
+
874
+ // Convert answer key to object for easier lookup
875
+ const correctAnswers = {};
876
+ const explanations = {};
877
+ const pointsMap = {};
878
+ if (answerKey.questions && Array.isArray(answerKey.questions)) {
879
+ answerKey.questions.forEach(q => {
880
+ correctAnswers[q.id] = q.correctAnswer;
881
+ explanations[q.id] = q.explanation;
882
+ pointsMap[q.id] = q.points || 1;
883
  });
 
 
 
884
  }
885
 
886
+ console.log('Processed user answers:', userAnswers);
887
+ console.log('Processed correct answers:', correctAnswers);
888
+
889
  // Analyze the answers
890
  let correctCount = 0;
891
  let totalPoints = 0;
892
  let maxPoints = 0;
893
  const feedback = [];
894
+ const questionsArray = quiz.questions || [];
895
+
896
+ if (questionsArray.length === 0) {
897
+ return {
898
+ content: [{ type: 'text', text: '❌ Quiz has no questions to analyze' }],
899
+ };
900
+ }
901
 
902
+ questionsArray.forEach((question, index) => {
903
+ const questionId = question.id || `question_${index}`;
904
+ const userAnswer = userAnswers[questionId];
905
+ const correctAnswer = correctAnswers[questionId];
906
+ const explanation = explanations[questionId];
907
+ const points = pointsMap[questionId] || 1;
908
  maxPoints += points;
909
 
910
+ if (!userAnswer) {
911
+ feedback.push(`⚠️ Q${index + 1}: "${question.question.substring(0, 50)}..." - Not answered`);
912
+ return;
913
+ }
 
914
 
915
+ if (!correctAnswer) {
916
+ // No correct answer in key - just show the user's answer for manual review
917
+ feedback.push(`πŸ“ Q${index + 1}: "${question.question.substring(0, 50)}..." \n User answered: "${userAnswer}" \n (No correct answer in key - manual review needed)`);
918
+ return;
919
+ }
920
+
921
+ // Check if answer is correct
922
+ let isCorrect = false;
923
+ if (Array.isArray(correctAnswer)) {
924
+ isCorrect = correctAnswer.some(ca =>
925
+ String(ca).toLowerCase().trim() === String(userAnswer).toLowerCase().trim()
926
+ );
 
 
 
 
 
 
927
  } else {
928
+ isCorrect = String(correctAnswer).toLowerCase().trim() === String(userAnswer).toLowerCase().trim();
929
+ }
930
+
931
+ if (isCorrect) {
932
+ correctCount++;
933
+ totalPoints += points;
934
+ feedback.push(`βœ… Q${index + 1}: Correct! (+${points} pts)`);
935
+ } else {
936
+ feedback.push(`❌ Q${index + 1}: "${question.question.substring(0, 40)}..."\n Your answer: "${userAnswer}"\n Correct: "${correctAnswer}"${explanation ? `\n πŸ’‘ ${explanation}` : ''}`);
 
 
937
  }
938
  });
939
 
940
+ const percentage = maxPoints > 0 ? Math.round((totalPoints / maxPoints) * 100) : 0;
941
  const grade = percentage >= 90 ? 'A' :
942
  percentage >= 80 ? 'B' :
943
  percentage >= 70 ? 'C' :
944
  percentage >= 60 ? 'D' : 'F';
945
 
946
+ // Include metadata if available
947
+ let metadataInfo = '';
948
+ if (userAnswersData.metadata) {
949
+ const meta = userAnswersData.metadata;
950
+ metadataInfo = `\n⏱️ Time taken: ${Math.floor((meta.timeTakenSeconds || 0) / 60)}m ${(meta.timeTakenSeconds || 0) % 60}s`;
951
+ if (meta.timeExceeded) {
952
+ metadataInfo += ' (Time limit exceeded!)';
953
+ }
954
+ metadataInfo += `\nπŸ“… Completed: ${meta.completedAt || 'Unknown'}`;
955
+ }
956
+
957
  return {
958
  content: [
959
  {
960
  type: 'text',
961
+ text: `πŸ“Š Quiz Analysis Results for "${quiz.title || 'Untitled Quiz'}"
962
 
963
  πŸ“ˆ Score: ${totalPoints}/${maxPoints} points (${percentage}%)
964
+ βœ… Correct: ${correctCount}/${questionsArray.length} questions
965
+ 🎯 Grade: ${grade}${metadataInfo}
966
 
967
+ πŸ“ Detailed Feedback:
968
+ ${feedback.join('\n\n')}
969
 
970
  ${percentage >= 70 ? 'πŸŽ‰ Great job!' : 'πŸ“š Keep studying and try again!'}`,
971
  },
972
  ],
973
  };
974
  } catch (error) {
975
+ console.error('analyzeQuiz error:', error);
976
  return {
977
+ content: [{ type: 'text', text: `❌ Error analyzing quiz: ${error.message}\n\nStack: ${error.stack}` }],
978
  };
979
  }
980
  }