Spaces:
Running
Running
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 +2 -1
- app/components/AboutModal.tsx +8 -8
- app/components/FileManager.tsx +110 -77
- app/components/HelpModal.tsx +50 -50
- app/components/Messages.tsx +111 -73
- app/components/SpotlightSearch.tsx +19 -19
- app/components/TextEditor.tsx +35 -33
- mcp-server.js +227 -81
.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={
|
| 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 |
-
<
|
| 389 |
-
|
| 390 |
-
<
|
| 391 |
-
|
| 392 |
-
|
| 393 |
-
|
| 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 |
-
<
|
| 412 |
-
|
| 413 |
-
|
| 414 |
-
|
| 415 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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={
|
| 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={
|
| 444 |
-
Lock
|
| 445 |
</button>
|
| 446 |
)}
|
| 447 |
|
| 448 |
{currentPath !== 'Applications' && (
|
| 449 |
-
|
| 450 |
-
|
| 451 |
-
|
| 452 |
-
|
| 453 |
-
|
| 454 |
-
|
| 455 |
-
|
| 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={
|
| 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={
|
| 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={
|
| 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-
|
| 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={
|
| 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={
|
| 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={
|
| 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={
|
| 86 |
</button>
|
| 87 |
</div>
|
| 88 |
-
<div className="flex items-center gap-2 ml-2">
|
| 89 |
-
<Info size={
|
| 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={
|
| 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={
|
| 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 |
-
<
|
| 126 |
-
|
| 127 |
-
<div
|
| 128 |
-
|
| 129 |
-
|
| 130 |
-
|
| 131 |
-
|
| 132 |
-
|
| 133 |
-
|
| 134 |
-
|
| 135 |
-
|
| 136 |
|
| 137 |
-
|
| 138 |
-
|
| 139 |
-
|
| 140 |
-
|
| 141 |
-
|
| 142 |
-
|
| 143 |
-
|
| 144 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 145 |
</div>
|
| 146 |
-
</div>
|
| 147 |
-
</div>
|
| 148 |
|
| 149 |
-
|
| 150 |
-
|
| 151 |
-
|
| 152 |
-
|
| 153 |
-
|
| 154 |
-
|
| 155 |
-
|
| 156 |
-
|
| 157 |
-
|
| 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 |
-
|
| 176 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
| 183 |
-
<
|
| 184 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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={
|
| 188 |
-
<span className="text-[10px] font-medium">Public Chat
|
| 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={
|
| 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={
|
| 295 |
</button>
|
| 296 |
</div>
|
| 297 |
</form>
|
| 298 |
-
<div className="text-[9px] text-gray-600 text-center mt-2 font-medium">
|
| 299 |
-
{inputText.length}/200 β’
|
| 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-[
|
| 118 |
>
|
| 119 |
-
<div className="flex items-center px-4 py-3 gap-3 border-b border-gray-200/20">
|
| 120 |
-
<MagnifyingGlass size={
|
| 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={
|
| 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={
|
| 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={
|
| 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={
|
| 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="
|
| 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={
|
| 280 |
-
<span>Save
|
|
|
|
| 281 |
</>
|
| 282 |
)}
|
| 283 |
</button>
|
|
@@ -293,13 +295,13 @@ export function TextEditor({
|
|
| 293 |
value={code}
|
| 294 |
onChange={(value) => setCode(value || '')}
|
| 295 |
options={{
|
| 296 |
-
minimap: { enabled:
|
| 297 |
-
fontSize: 14,
|
| 298 |
fontFamily: "'JetBrains Mono', 'Fira Code', 'Cascadia Code', monospace",
|
| 299 |
lineNumbers: 'on',
|
| 300 |
scrollBeyondLastLine: false,
|
| 301 |
automaticLayout: true,
|
| 302 |
-
padding: { top:
|
| 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}
|
| 323 |
</div>
|
| 324 |
-
<div>
|
| 325 |
-
{isLatexFile && <span>π‘ Download and compile .tex file locally
|
| 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 |
-
|
| 551 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 552 |
createdAt: new Date().toISOString(),
|
| 553 |
-
passkey: passkey,
|
| 554 |
version: '1.0',
|
| 555 |
};
|
| 556 |
|
| 557 |
-
//
|
| 558 |
-
const
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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(
|
| 566 |
-
isPublic: false,
|
| 567 |
}),
|
| 568 |
});
|
| 569 |
|
| 570 |
-
const
|
| 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
|
| 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
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 722 |
};
|
| 723 |
}
|
| 724 |
|
|
|
|
| 725 |
const quizFile = quizData.files.find(f => f.name === 'quiz.json');
|
| 726 |
if (!quizFile) {
|
| 727 |
return {
|
| 728 |
-
content: [{ type: 'text', text:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 729 |
};
|
| 730 |
}
|
| 731 |
|
| 732 |
-
//
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 733 |
const answersFile = quizData.files.find(f => f.name === 'quiz_answers.json');
|
| 734 |
if (!answersFile) {
|
| 735 |
return {
|
| 736 |
-
content: [{ type: 'text', text:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 737 |
};
|
| 738 |
}
|
| 739 |
|
| 740 |
-
|
| 741 |
-
|
| 742 |
-
|
| 743 |
|
| 744 |
-
// Convert answers array to object for easier lookup
|
| 745 |
-
const
|
| 746 |
-
if (
|
| 747 |
// QuizApp format: { answers: [{ questionId, answer }], metadata: {...} }
|
| 748 |
-
|
| 749 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 763 |
-
const
|
| 764 |
-
const
|
|
|
|
|
|
|
|
|
|
| 765 |
maxPoints += points;
|
| 766 |
|
| 767 |
-
|
| 768 |
-
|
| 769 |
-
|
| 770 |
-
|
| 771 |
-
(Array.isArray(correctAnswer) && correctAnswer.includes(userAnswer));
|
| 772 |
|
| 773 |
-
|
| 774 |
-
|
| 775 |
-
|
| 776 |
-
|
| 777 |
-
|
| 778 |
-
|
| 779 |
-
|
| 780 |
-
|
| 781 |
-
|
| 782 |
-
|
| 783 |
-
|
| 784 |
-
|
| 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 |
-
|
| 793 |
-
|
| 794 |
-
|
| 795 |
-
|
| 796 |
-
|
| 797 |
-
|
| 798 |
-
|
| 799 |
-
|
| 800 |
-
}
|
| 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
|
| 820 |
-
π― Grade: ${grade}
|
| 821 |
|
| 822 |
-
π
|
| 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 |
}
|