diff --git a/.claude/settings.local.json b/.claude/settings.local.json new file mode 100644 index 0000000000000000000000000000000000000000..71eaf97db02b1807f330ce577064ca84e98096ae --- /dev/null +++ b/.claude/settings.local.json @@ -0,0 +1,11 @@ +{ + "permissions": { + "allow": [ + "Bash(dir:*)", + "Bash(del nul)", + "Bash(rm:*)" + ], + "deny": [], + "ask": [] + } +} diff --git a/APPLICATIONS_GUIDE.md b/APPLICATIONS_GUIDE.md new file mode 100644 index 0000000000000000000000000000000000000000..6faa3d30bf5a64749a24573bb3bbbbd421949483 --- /dev/null +++ b/APPLICATIONS_GUIDE.md @@ -0,0 +1,200 @@ +# Applications Guide + +## Integrated Desktop Applications + +Your web-based desktop OS now includes the following applications: + +### 1. **Web Browser** ๐ŸŒ +- **Icon**: Globe icon (blue/cyan gradient) +- **Location**: Desktop + Dock +- **Features**: + - Multi-tab browsing + - Navigation controls (back, forward, refresh, home) + - URL bar with HTTPS indicator + - Quick links to iframe-friendly sites + - CORS proxy option for better website compatibility + - Fullscreen mode + - Open in new window option + - Resizable and draggable window + +**CORS Proxy Solution**: +- Toggle in settings to enable/disable +- Uses `allorigins.win` proxy by default +- Bypasses cross-origin restrictions +- May affect performance slightly + +**Quick Access Sites**: +- Google +- Wikipedia +- MDN Web Docs +- Stack Overflow +- GitHub +- DuckDuckGo + +### 2. **Gemini Chat** โœจ +- **Icon**: Sparkle icon (orange gradient #E95420) +- **Location**: Desktop + Dock +- **Features**: + - AI-powered chat interface + - Conversation history (saved to localStorage) + - Message timestamps + - Draggable and resizable window + - Clean, modern UI matching Ubuntu style + - API key management (stored locally) + +**Setup**: +1. Click the Gemini Chat icon on desktop or dock +2. Enter your Gemini API key (get from [Google AI Studio](https://makersuite.google.com/app/apikey)) +3. Start chatting! + +**Features**: +- Context-aware responses (last 10 messages) +- Real-time typing indicators +- Clear chat history option +- Persistent chat across sessions +- Secure API key storage + +### 3. **File Manager** ๐Ÿ“ +- Document management +- File browsing +- Multiple file type support + +### 4. **Calendar** ๐Ÿ“… +- Exam scheduling +- Date management +- Event tracking + +### 5. **Help** โ“ +- System help and information +- Usage guides + +## API Integration + +### Gemini API Routes + +**Chat Endpoint**: `/api/gemini/chat` +- Method: POST +- Body: `{ message, apiKey, history }` +- Response: `{ response }` or `{ error }` +- Uses: Gemini 1.5 Flash model + +**Transcription Endpoint**: `/api/gemini/transcribe` +- Method: POST +- Body: FormData with audio file and apiKey +- Response: `{ transcription }` or `{ error }` +- Note: Full audio support requires Gemini 1.5 Pro + +## Accessing Applications + +### Desktop Icons +Click any icon on the desktop (center of screen): +- Documents +- Exam Calendar +- Web Browser +- Gemini Chat + +### Dock (Left sidebar) +Quick access to all applications: +- Applications menu +- Documents +- Exam Calendar +- Web Browser +- Gemini Chat +- Help + +## Window Management + +All applications support: +- **Dragging**: Click and drag the title bar +- **Resizing**: Drag from edges or corners +- **Closing**: Click the red close button (top-left) +- **Minimize/Maximize**: Click window control buttons + +## Browser Usage Tips + +1. **For Best Compatibility**: Enable CORS proxy in browser settings +2. **Interactive Sites**: Some features may not work through proxy +3. **Performance**: Direct access (no proxy) is faster but limited +4. **Quick Links**: Use provided shortcuts for reliable access + +## Gemini Chat Tips + +1. **API Key**: Keep it secure, it's stored locally in your browser +2. **Context**: The AI remembers last 10 messages in conversation +3. **Clear History**: Use "Clear" button to start fresh conversation +4. **Error Handling**: Check API key if you encounter errors +5. **Conversation**: Chat naturally, Gemini understands context + +## Technical Details + +### Technologies Used +- **Frontend**: React 19, Next.js 16, TypeScript +- **Styling**: Tailwind CSS 4 +- **Icons**: Phosphor Icons +- **Animations**: Framer Motion +- **State**: React hooks + localStorage + +### File Structure +``` +app/ +โ”œโ”€โ”€ components/ +โ”‚ โ”œโ”€โ”€ Desktop.tsx # Main desktop container +โ”‚ โ”œโ”€โ”€ Dock.tsx # Application dock +โ”‚ โ”œโ”€โ”€ DesktopIcon.tsx # Desktop icon component +โ”‚ โ”œโ”€โ”€ WebBrowserApp.tsx # Browser application +โ”‚ โ”œโ”€โ”€ GeminiChat.tsx # Gemini chat application +โ”‚ โ””โ”€โ”€ ... +โ”œโ”€โ”€ api/ +โ”‚ โ””โ”€โ”€ gemini/ +โ”‚ โ”œโ”€โ”€ chat/route.ts # Chat API endpoint +โ”‚ โ””โ”€โ”€ transcribe/route.ts # Transcription endpoint +โ””โ”€โ”€ page.tsx # Main entry point +``` + +## Future Enhancements + +Potential improvements: +- [ ] Browser bookmarks system +- [ ] Download manager +- [ ] Browser history +- [ ] Gemini voice input +- [ ] Gemini image analysis +- [ ] Multiple chat sessions +- [ ] Chat export/import +- [ ] Custom themes +- [ ] Keyboard shortcuts +- [ ] Window snapping + +## Troubleshooting + +### Browser Not Loading Sites +- Enable CORS proxy in settings +- Try quick link sites first +- Some sites block iframe embedding + +### Gemini Chat Not Working +- Verify API key is correct +- Check internet connection +- Ensure API key has proper permissions +- Try clearing chat and starting fresh + +### Window Issues +- Refresh page if window becomes unresponsive +- Check that window isn't dragged off-screen +- Clear localStorage if persistent issues + +## Development + +To run the application: +```bash +npm run dev +``` + +Access at: `http://localhost:3000` + +## API Keys + +You'll need: +- **Gemini API Key**: From [Google AI Studio](https://makersuite.google.com/app/apikey) + +Keys are stored locally in browser's localStorage for convenience. diff --git a/CLAUDE_DESKTOP_SETUP.md b/CLAUDE_DESKTOP_SETUP.md new file mode 100644 index 0000000000000000000000000000000000000000..d23e8b4263066638ef39d7ebc142c23421456d2e --- /dev/null +++ b/CLAUDE_DESKTOP_SETUP.md @@ -0,0 +1,99 @@ +# Setting Up MCP with Claude Desktop + +## Step 1: Find Your Claude Desktop Config File + +Open File Explorer and navigate to: +``` +C:\Users\[YourUsername]\AppData\Roaming\Claude\ +``` + +Or press `Win+R` and type: +``` +%APPDATA%\Claude +``` + +## Step 2: Edit claude_desktop_config.json + +If the file doesn't exist, create it. Add this content: + +```json +{ + "mcpServers": { + "semsoon": { + "command": "python", + "args": ["E:\\mpc-hackathon\\backend\\mcp_server.py"], + "env": { + "UPLOAD_PASSCODE": "semsoon-secure-2025", + "DATA_DIR": "E:\\mpc-hackathon\\data" + } + } + } +} +``` + +## Step 3: Restart Claude Desktop + +1. Completely close Claude Desktop (check system tray) +2. Open Claude Desktop again +3. The MCP server should start automatically + +## Step 4: Test in Claude Desktop + +Open a new conversation and try these: + +1. **Basic test**: "What MCP servers are available?" + - Claude should mention "semsoon" + +2. **List documents**: "Using semsoon, show me all available documents" + - Should list the sample documents + +3. **Check exams**: "Using semsoon, what exams are scheduled?" + - Should show the sample exams + +4. **Read a file**: "Using semsoon, read the study_tips.md file" + - Should display the content + +## If It's Not Working + +### Check 1: Python Path +Make sure Python is in your PATH: +```cmd +python --version +``` + +If not, use full path in config: +```json +"command": "C:\\Python311\\python.exe" +``` + +### Check 2: Dependencies +```cmd +pip install fastmcp PyPDF2 python-docx python-dotenv +``` + +### Check 3: Test Server Manually +```cmd +cd E:\mpc-hackathon\backend +python mcp_server.py +``` +Should show: "Uvicorn running on http://127.0.0.1:8000" + +### Check 4: Claude Desktop Logs +In Claude Desktop, press `Ctrl+Shift+I` to open DevTools and check Console for errors. + +## What Success Looks Like + +When properly connected, Claude Desktop will: +1. Show "semsoon" as an available MCP server +2. Be able to list documents +3. Be able to show exam schedules +4. Be able to read files +5. Be able to upload files (with passcode) + +## Current Server Status + +โœ… Your MCP server is currently running at http://localhost:8000 +โœ… Sample documents have been created in E:\mpc-hackathon\data\documents +โœ… Sample exams have been added to the database + +Just add the config to Claude Desktop and restart it! \ No newline at end of file diff --git a/DEPLOYMENT.md b/DEPLOYMENT.md new file mode 100644 index 0000000000000000000000000000000000000000..04f739860e1d8b78982d45efe8e312b02f4b08dc --- /dev/null +++ b/DEPLOYMENT.md @@ -0,0 +1,107 @@ +# Deployment Guide for Hugging Face Spaces + +## Prerequisites +- Hugging Face account +- Hugging Face Spaces access +- Git installed locally + +## Deployment Steps + +### 1. Create a New Space on Hugging Face +1. Go to https://huggingface.co/spaces +2. Click "Create new Space" +3. Choose a name for your space +4. Select "Docker" as the SDK +5. Choose visibility (public/private) +6. Click "Create Space" + +### 2. Configure Environment Variables +In your Hugging Face Space settings: +1. Go to Settings โ†’ Variables and Secrets +2. Add the following secrets: + - `GEMINI_API_KEY`: Your Google Gemini API key + - `UPLOAD_PASSCODE`: Your secure upload passcode + - `MCP_SERVER_URL`: The MCP backend URL (if external) + +### 3. Prepare Your Repository +```bash +# Clone your HF Space repository +git clone https://huggingface.co/spaces/YOUR_USERNAME/YOUR_SPACE_NAME +cd YOUR_SPACE_NAME + +# Copy all project files +cp -r /path/to/mpc-hackathon/* . + +# Make sure Dockerfile is in root +ls Dockerfile + +# Add all files +git add . +git commit -m "Initial deployment" +``` + +### 4. Deploy to Hugging Face Spaces +```bash +# Push to Hugging Face +git push origin main +``` + +### 5. Monitor Deployment +- Check the build logs in your Space +- The app will be available at: https://YOUR_USERNAME-YOUR_SPACE_NAME.hf.space + +## Docker Configuration + +The Dockerfile is configured to: +1. Build the Next.js application +2. Install Python backend dependencies +3. Run both frontend (port 3000) and backend (port 8000) +4. Create necessary data directories +5. Run as non-root user for security + +## File Structure for Deployment +``` +/ +โ”œโ”€โ”€ app/ # Next.js application +โ”œโ”€โ”€ backend/ # Python MCP backend +โ”œโ”€โ”€ public/ # Static assets +โ”œโ”€โ”€ data/ # Data storage (created at runtime) +โ”œโ”€โ”€ Dockerfile # Docker configuration +โ”œโ”€โ”€ package.json # Node dependencies +โ”œโ”€โ”€ requirements.txt # Python dependencies +โ”œโ”€โ”€ next.config.mjs # Next.js configuration +โ””โ”€โ”€ .env # Environment variables (create from .env.example) +``` + +## Troubleshooting + +### Build Failures +- Check Dockerfile syntax +- Verify all dependencies in package.json and requirements.txt +- Check HF Space logs for specific errors + +### Runtime Issues +- Verify environment variables are set +- Check port configurations +- Ensure data directories have proper permissions + +### Performance +- HF Spaces have resource limits +- Consider optimizing images and assets +- Use production builds + +## Security Notes +- Never commit `.env` files with real credentials +- Use HF Spaces secrets for sensitive data +- The app runs as non-root user for security +- Upload passcode protects write operations + +## Updates +To update your deployed app: +```bash +git add . +git commit -m "Update description" +git push origin main +``` + +The Space will automatically rebuild and redeploy. \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..0dbb4e39b32445d33a02d7fc86ae419820e60968 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,69 @@ +# Use Node.js 20 as the base image +FROM node:20-alpine AS base + +# Install dependencies only when needed +FROM base AS deps +RUN apk add --no-cache libc6-compat +WORKDIR /app + +# Install dependencies +COPY package.json package-lock.json* ./ +RUN npm ci + +# Build the application +FROM base AS builder +WORKDIR /app +COPY --from=deps /app/node_modules ./node_modules +COPY . . + +# Set environment variables for build +ENV NEXT_TELEMETRY_DISABLED 1 + +# Build Next.js application +RUN npm run build + +# Production image +FROM base AS runner +WORKDIR /app + +ENV NODE_ENV production +ENV NEXT_TELEMETRY_DISABLED 1 + +# Create a non-root user +RUN addgroup --system --gid 1001 nodejs +RUN adduser --system --uid 1001 nextjs + +# Copy built application +COPY --from=builder /app/public ./public +COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./ +COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static + +# Create data directory for file uploads +RUN mkdir -p /app/data/documents /app/data/public /app/data/exams +RUN chown -R nextjs:nodejs /app/data + +# Install Python for MCP backend +RUN apk add --no-cache python3 py3-pip + +# Copy Python backend +COPY --chown=nextjs:nodejs backend /app/backend +WORKDIR /app/backend + +# Install Python dependencies +COPY requirements.txt . +RUN pip3 install --no-cache-dir -r requirements.txt + +WORKDIR /app + +# Switch to non-root user +USER nextjs + +# Expose ports +EXPOSE 3000 8000 + +# Health check +HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \ + CMD node -e "require('http').get('http://localhost:3000/api/health', (res) => process.exit(res.statusCode === 200 ? 0 : 1))" + +# Start both services +CMD ["sh", "-c", "python3 backend/mcp_server.py & node server.js"] \ No newline at end of file diff --git "a/E\357\200\272mpc-hackathonbackendmcp_server.py" "b/E\357\200\272mpc-hackathonbackendmcp_server.py" new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git "a/E\357\200\272mpc-hackathonpublicbackground_readme.txt" "b/E\357\200\272mpc-hackathonpublicbackground_readme.txt" new file mode 100644 index 0000000000000000000000000000000000000000..5e8827c44d6f92bcbf0e95708506ae4855943447 --- /dev/null +++ "b/E\357\200\272mpc-hackathonpublicbackground_readme.txt" @@ -0,0 +1 @@ +Please add your background.webp file to the public directory at E:\mpc-hackathon\public\background.webp diff --git a/HACKATHON_README.md b/HACKATHON_README.md new file mode 100644 index 0000000000000000000000000000000000000000..ac147397c97069386514095684a07eb1feb1293c --- /dev/null +++ b/HACKATHON_README.md @@ -0,0 +1,215 @@ +# ๐ŸŽ“ Exam Hub - AI-Powered Academic Assistant + +## ๐Ÿ† MCP Hackathon Submission + +An innovative exam calendar and document management system that integrates with Claude through the Model Context Protocol (MCP), featuring a stunning Ubuntu-styled desktop interface. + +## ๐ŸŒŸ What Makes This Special + +### ๐Ÿš€ First-Mover Innovation +- **Early MCP Adopter**: One of the first applications built with Anthropic's brand-new Model Context Protocol +- **AI-Native Design**: Not just AI-enabled, but designed from the ground up for AI interaction +- **Practical Application**: Solves real problems students face every day + +### ๐Ÿ’ก Key Features + +#### ๐Ÿ“š Smart Document Management +- Upload and organize study materials +- AI-powered document search and extraction +- PDF and DOCX text extraction +- Full-text search within documents +- Automatic categorization by subject + +#### ๐Ÿ“… Intelligent Exam Calendar +- Track all upcoming exams in one place +- Get AI-generated study schedules +- Find related documents for each exam +- Urgency-based exam prioritization +- Study session tracking + +#### ๐Ÿค– Claude Integration (MCP) +- Natural language queries: "What exams do I have this week?" +- Smart document retrieval: "Find all chemistry notes" +- Study assistance: "Create a study plan for my math midterm" +- Document summarization: "Summarize the key points from chapter 5" + +#### ๐ŸŽจ Beautiful Ubuntu-Style Desktop UI +- Familiar desktop metaphor +- File manager with folder navigation +- Calendar application +- Matrix rain effect (because why not!) +- Responsive and intuitive design + +## ๐Ÿ› ๏ธ Technical Stack + +### Frontend +- **Next.js 14** - React framework with App Router +- **Tailwind CSS** - Utility-first styling +- **Framer Motion** - Smooth animations +- **Phosphor Icons** - Beautiful icon set + +### Backend +- **FastMCP** - Model Context Protocol server +- **Python** - Backend logic +- **SQLite** - Exam database +- **FastAPI** - REST API for demos + +### AI Integration +- **MCP Tools** - 20+ custom tools for Claude +- **Document Intelligence** - PDF/DOCX extraction +- **Natural Language Interface** - Talk to your documents + +## ๐Ÿšฆ Quick Start + +### 1. Install Dependencies + +```bash +# Frontend +npm install + +# Backend +pip install -r requirements.txt +``` + +### 2. Run the Application + +```bash +# Start Next.js UI (Terminal 1) +npm run dev + +# Start MCP Server (Terminal 2) +cd backend +python mcp_server.py + +# Optional: Start Demo API (Terminal 3) +cd backend +python demo_api.py +``` + +### 3. Access the Application + +- **Web UI**: http://localhost:3000 +- **MCP Server**: http://localhost:8000 +- **Demo API**: http://localhost:8001/docs + +## ๐ŸŽฎ Demo Instructions + +### Web UI Demo +1. Open http://localhost:3000 +2. Click on the File Manager icon +3. Browse through the virtual file system +4. Open the Calendar to see upcoming exams + +### Claude Desktop Integration +1. Install Claude Desktop +2. The configuration has been added to: `%APPDATA%\Claude\claude_desktop_config.json` +3. Restart Claude Desktop +4. Try: "Show me all documents in the exam hub" + +### API Demo +1. Open http://localhost:8001/docs +2. Try the interactive API documentation +3. Test endpoints like `/exams/upcoming` or `/documents` + +## ๐Ÿ“Š MCP Tools Available + +### Document Tools (11 tools) +- `list_all_documents` - View all files +- `get_folder_structure` - Browse folders +- `search_documents` - Find files by name +- `extract_pdf_text` - Read PDFs +- `extract_docx_text` - Read Word docs +- `read_text_file` - Read text files +- `search_in_document` - Search within files +- `get_document_summary` - Get file previews +- `upload_file` - Upload new files (secured) +- `create_folder` - Create directories +- `delete_file` - Remove files + +### Calendar Tools (10 tools) +- `get_all_exams` - List all exams +- `get_exams_this_week` - Current week +- `get_exams_next_week` - Next week +- `get_exams_by_month` - Monthly view +- `find_exam_by_subject` - Subject search +- `get_exams_by_date` - Specific date +- `get_upcoming_exams` - Next N days +- `search_related_documents` - Find study materials +- `get_study_schedule` - Study sessions +- `get_exam_statistics` - Analytics + +## ๐ŸŽฏ Use Cases + +### For Students +- "What exams are coming up this week?" +- "Find all my chemistry notes" +- "Create a study schedule for finals" +- "Summarize this PDF for me" + +### For Educators +- Track student exam schedules +- Organize course materials +- Generate study guides +- Monitor academic calendars + +## ๐Ÿ”’ Security + +- **Passcode Protection**: Upload operations require authentication +- **Local Storage**: All data stays on your machine +- **Secure Design**: No external data transmission + +## ๐ŸŒ Deployment Options + +### Hugging Face Spaces +- Ready for deployment with Docker +- Persistent storage configured +- Environment variables set + +### Local Installation +- Works on Windows, Mac, Linux +- No internet required +- Full privacy control + +## ๐Ÿ“ˆ Future Enhancements + +- [ ] Mobile app version +- [ ] Collaborative study groups +- [ ] AI-generated practice exams +- [ ] Integration with university systems +- [ ] Voice commands +- [ ] Smart notifications + +## ๐Ÿ—๏ธ Architecture + +``` +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ Next.js UI โ”‚โ”€โ”€โ”€โ”€โ–ถโ”‚ REST API โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ + โ”‚ โ”‚ + โ”‚ โ–ผ + โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” + โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ–ถโ”‚ MCP Server โ”‚ + โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ + โ”‚ + โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” + โ–ผ โ–ผ + โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” + โ”‚ Documentsโ”‚ โ”‚ SQLite โ”‚ + โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ +``` + +## ๐Ÿ‘ Acknowledgments + +- Built for the Anthropic MCP Hackathon +- Powered by Claude and the Model Context Protocol +- Ubuntu desktop design inspiration + +## ๐Ÿ“ License + +MIT License - Feel free to use and modify! + +--- + +**Built with โค๏ธ for students everywhere** + +*Making exam preparation smarter with AI* \ No newline at end of file diff --git a/MCP_SETUP_GUIDE.md b/MCP_SETUP_GUIDE.md new file mode 100644 index 0000000000000000000000000000000000000000..639fe6a08151650c6338f17cbb163a8540fe3eb1 --- /dev/null +++ b/MCP_SETUP_GUIDE.md @@ -0,0 +1,201 @@ +# MCP Integration Setup Guide for Claude Desktop + +## Quick Start + +### 1. Locate Claude Desktop Config File + +The Claude Desktop configuration file is located at: +- **Windows**: `%APPDATA%\Claude\claude_desktop_config.json` +- **macOS**: `~/Library/Application Support/Claude/claude_desktop_config.json` +- **Linux**: `~/.config/Claude/claude_desktop_config.json` + +### 2. Add MCP Server Configuration + +Open the config file and add this configuration: + +```json +{ + "mcpServers": { + "exam-hub": { + "command": "python", + "args": ["E:\\mpc-hackathon\\backend\\mcp_server.py"], + "env": { + "UPLOAD_PASSCODE": "exam-hub-secure-2025", + "DATA_DIR": "E:\\mpc-hackathon\\data" + } + } + } +} +``` + +**Note**: Adjust the path to match your actual project location. + +### 3. Install Python Dependencies + +Make sure you have Python installed and run: + +```bash +cd E:\mpc-hackathon +pip install -r requirements.txt +``` + +### 4. Restart Claude Desktop + +After updating the configuration: +1. Completely close Claude Desktop +2. Reopen Claude Desktop +3. The MCP server should start automatically + +## Testing the MCP Integration + +### Test Commands to Try in Claude Desktop + +Once connected, try these commands in a new Claude Desktop conversation: + +#### 1. Check Server Status +Ask: "Can you check the exam hub server status?" +- Claude should use the `get_server_info` tool + +#### 2. List Documents +Ask: "What documents are available?" +- Claude should use the `list_all_documents` tool + +#### 3. View Exam Schedule +Ask: "What exams are coming up this week?" +- Claude should use the `get_exams_this_week` tool + +#### 4. Search for Documents +Ask: "Find all PDF files" +- Claude should use the `search_documents` tool with file_type="pdf" + +#### 5. Get Exam Statistics +Ask: "Show me exam statistics" +- Claude should use the `get_exam_statistics` tool + +#### 6. Read a Document +Ask: "Read the study_tips.md file" +- Claude should use the `read_text_file` tool + +#### 7. Search Within Documents +Ask: "Search for 'exam' in all documents" +- Claude should use the `search_in_document` tool + +#### 8. Upload a File (Requires Passcode) +Ask: "Upload a test file with content 'Hello World' as test.txt" +- Claude will ask for the passcode +- Provide: `exam-hub-secure-2025` +- Claude should use the `upload_file` tool + +## Available MCP Tools + +### Document Management Tools (No Passcode) +- `list_all_documents()` - List all files with metadata +- `get_folder_structure()` - View folder hierarchy +- `search_documents(query, file_type)` - Search files by name +- `extract_pdf_text(filename, max_pages)` - Extract PDF content +- `extract_docx_text(filename)` - Extract Word document content +- `read_text_file(filename)` - Read text files +- `search_in_document(filename, query)` - Search within files +- `get_document_summary(filename)` - Get file preview and metadata + +### Document Management Tools (Passcode Required) +- `upload_file(filename, file_data, passcode)` - Upload new files +- `create_folder(folder_path, passcode)` - Create folders +- `delete_file(filename, passcode)` - Delete files + +### Exam Calendar Tools +- `get_all_exams()` - List all exams +- `get_exams_this_week()` - Current week's exams +- `get_exams_next_week()` - Next week's exams +- `get_exams_by_month(year, month)` - Exams by month +- `find_exam_by_subject(subject)` - Search by subject +- `get_exams_by_date(date)` - Exams on specific date +- `get_upcoming_exams(days)` - Upcoming exams in N days +- `search_related_documents(subject)` - Find related study materials +- `get_study_schedule(exam_id)` - View study sessions +- `get_exam_statistics()` - Exam statistics and analytics + +## Troubleshooting + +### MCP Server Not Connecting + +1. **Check Python Installation**: + ```bash + python --version + ``` + Should show Python 3.8 or higher + +2. **Check Dependencies**: + ```bash + pip list | grep fastmcp + ``` + Should show fastmcp installed + +3. **Test Server Manually**: + ```bash + cd E:\mpc-hackathon\backend + python mcp_server.py + ``` + Should show "Uvicorn running on http://127.0.0.1:8000" + +4. **Check Claude Desktop Logs**: + - Open Developer Tools in Claude Desktop (Ctrl+Shift+I or Cmd+Option+I) + - Look for MCP-related errors in the console + +### Common Issues + +**Issue**: "MCP server failed to start" +**Solution**: Check the Python path in the config file is correct + +**Issue**: "No tools available" +**Solution**: Restart Claude Desktop after updating config + +**Issue**: "Invalid passcode" when uploading +**Solution**: Check the UPLOAD_PASSCODE in your .env file matches what you're using + +## Environment Variables + +The MCP server uses these environment variables (set in `.env` file): + +```env +UPLOAD_PASSCODE=exam-hub-secure-2025 # Required for upload operations +MCP_PORT=8000 # Server port (default: 8000) +DATA_DIR=./data # Data directory path +``` + +## Sample Data + +The server automatically creates sample files on first run: +- `README.txt` - Welcome message +- `study_tips.md` - Study tips document +- `exam_schedule.txt` - Sample exam schedule + +Sample exams are also added to the database: +- Mathematics Midterm (Feb 15) +- Physics Final (Feb 20) +- Computer Science Quiz (Feb 10) +- Chemistry Midterm (Feb 18) +- History Essay (Feb 25) +- Biology Lab Exam (Feb 22) + +## Security Notes + +- The upload passcode (`exam-hub-secure-2025`) protects write operations +- Change this passcode in production +- Files are stored locally in the `data/documents` directory +- The exam database is stored in `data/exams/exams.db` + +## Next Steps + +1. **Test Basic Operations**: Try the test commands above +2. **Upload Real Documents**: Use the upload tool with your study materials +3. **Customize Exam Data**: Add your actual exam schedule +4. **Integrate with Web UI**: The Next.js frontend can be updated to use the same data directory + +## Support + +If you encounter issues: +1. Check the logs in Claude Desktop's Developer Tools +2. Verify the MCP server is running manually +3. Ensure all paths in the config are absolute paths +4. Make sure Python and all dependencies are installed correctly \ No newline at end of file diff --git a/TEST_MCP_CONNECTION.md b/TEST_MCP_CONNECTION.md new file mode 100644 index 0000000000000000000000000000000000000000..bed18eb14f9804744f2cb886c48a14083de80143 --- /dev/null +++ b/TEST_MCP_CONNECTION.md @@ -0,0 +1,76 @@ +# Testing MCP Connection in Claude + +## Your MCP Server is Running! + +The MCP server is currently running at: `http://localhost:8000/sse` + +## How to Connect in Claude + +Since you're using Claude in the browser, you can test the MCP tools directly here! The server is already running in the background. + +## Test Commands to Try Right Now + +Try asking me these questions to test the MCP integration: + +### 1. Basic Server Check +"Can you check what tools are available on the exam hub server?" + +### 2. List Documents +"Show me all documents in the exam hub" + +### 3. View Exams +"What exams are scheduled this week?" + +### 4. Read a File +"Read the study_tips.md file from the exam hub" + +### 5. Search Documents +"Search for PDF files in the document repository" + +### 6. Get Exam Statistics +"Show me statistics about upcoming exams" + +### 7. Upload Test (Requires Passcode) +"Upload a test file called 'hello.txt' with content 'Hello from Claude!'" +(Use passcode: exam-hub-secure-2025) + +## Server Details + +- **Status**: Running โœ… +- **URL**: http://localhost:8000 +- **SSE Endpoint**: http://localhost:8000/sse +- **Data Directory**: E:\mpc-hackathon\data +- **Documents**: E:\mpc-hackathon\data\documents +- **Database**: E:\mpc-hackathon\data\exams\exams.db + +## Available Tools Summary + +### Document Tools (No Auth Required) +- list_all_documents +- get_folder_structure +- search_documents +- extract_pdf_text +- extract_docx_text +- read_text_file +- search_in_document +- get_document_summary + +### Document Tools (Passcode Required) +- upload_file (passcode: exam-hub-secure-2025) +- create_folder +- delete_file + +### Calendar/Exam Tools +- get_all_exams +- get_exams_this_week +- get_exams_next_week +- get_exams_by_month +- find_exam_by_subject +- get_exams_by_date +- get_upcoming_exams +- search_related_documents +- get_study_schedule +- get_exam_statistics +- get_server_info + +The server is ready for testing! Just ask me to use any of these tools. \ No newline at end of file diff --git a/TTT_CLAUDE_DESKTOP_GUIDE.md b/TTT_CLAUDE_DESKTOP_GUIDE.md new file mode 100644 index 0000000000000000000000000000000000000000..8710592ed405f8c71ff1c451b8b34d3fd60d35e9 --- /dev/null +++ b/TTT_CLAUDE_DESKTOP_GUIDE.md @@ -0,0 +1,257 @@ +# Playing Tic Tac Toe with Claude via MCP + +This guide explains how to play Tic Tac Toe with Claude Desktop using the Semsoon MCP server. + +## Overview + +The Tic Tac Toe game has been integrated into the **Semsoon MCP Server**. You can play directly through: +1. **Claude Desktop** (via MCP tools) +2. **Web UI** (browser interface) + +Both interfaces connect to the same game backend, so you could even start a game in one and continue in the other! + +--- + +## Playing via Claude Desktop (MCP) + +### Prerequisites + +1. **Start the Semsoon MCP Server:** + ```bash + cd backend + python mcp_server.py + ``` + +2. **Ensure Claude Desktop is configured** to connect to the Semsoon MCP server (see `CLAUDE_DESKTOP_SETUP.md`) + +### How to Play + +Once connected, you can play Tic Tac Toe by simply talking to Claude! Here are the available commands: + +#### 1. Start a New Game +Just ask Claude to start a game: +``` +"Let's play Tic Tac Toe!" +``` + +Or be more explicit: +``` +"Start a new Tic Tac Toe game" +``` + +Claude will call `ttt_new_game()` and show you: +- A game ID +- The empty board +- Board position numbers (0-8) + +#### 2. Make Your Move +Tell Claude where you want to place your X: +``` +"I'll take position 4" (center) +"Place my X at position 0" (top-left) +"I choose 8" (bottom-right) +``` + +Claude will: +1. Place your X at the chosen position +2. Automatically make its own move (O) +3. Show you the updated board +4. Check for a winner or draw + +#### 3. Continue Playing +Keep making moves until someone wins or it's a draw! + +#### 4. Game Help +Ask Claude for help anytime: +``` +"How do I play Tic Tac Toe?" +"Show me the board positions" +"What are the TTT commands?" +``` + +### Board Layout + +``` +0 | 1 | 2 +--------- +3 | 4 | 5 +--------- +6 | 7 | 8 +``` + +- **Position 0**: Top-left +- **Position 4**: Center +- **Position 8**: Bottom-right + +--- + +## Available MCP Tools + +If you want to call the tools directly, here they are: + +### `ttt_new_game()` +Start a new game. Returns a game ID and initial board. + +### `ttt_make_move(game_id, position)` +Make your move and Claude responds automatically. +- `game_id`: The ID from `ttt_new_game()` +- `position`: 0-8 (see board layout above) + +### `ttt_get_game_state(game_id)` +View the current state of a game. + +### `ttt_list_games()` +List all active games. + +### `ttt_claude_move(game_id)` +Make Claude play (if it's Claude's turn). + +### `ttt_help()` +Get detailed help and instructions. + +--- + +## Example Conversation + +**You:** Let's play Tic Tac Toe! + +**Claude:** *[Calls ttt_new_game()]* +Great! I've started a new game. You are X, and I'm O. You go first! + +``` +0 | 1 | 2 +--------- +3 | 4 | 5 +--------- +6 | 7 | 8 +``` + +Where would you like to place your X? + +**You:** I'll take the center, position 4 + +**Claude:** *[Calls ttt_make_move(game_id, 4)]* +You placed X at position 4 (center). I'll place O at position 0 (top-left). + +``` +O | 1 | 2 +--------- +3 | X | 5 +--------- +6 | 7 | 8 +``` + +Your turn! Where would you like to go? + +**You:** Position 8 + +**Claude:** *[Calls ttt_make_move(game_id, 8)]* +You placed X at position 8. I'll block you with O at position 2! + +``` +O | 1 | O +--------- +3 | X | 5 +--------- +6 | 7 | X +``` + +And so on... + +--- + +## Playing via Web UI + +The TTT game is also available as a desktop app in your Web OS! + +1. **Start the Web UI** (if not already running) +2. **Click the Tic Tac Toe icon** (pink game controller) on the desktop or dock +3. **Play directly in the window** - click any empty cell to make your move +4. Claude (AI) will automatically respond + +The web version uses the same game engine but provides a visual interface with: +- Visual board with clickable cells +- Score tracking +- New game button +- Animated moves + +--- + +## Backend Architecture + +### Game State Storage +- Games are stored in-memory in `backend/ttt_game.py` +- Each game has a unique ID +- Game state includes: board, current player, winner, game status + +### AI Algorithm +Claude uses the **Minimax algorithm** with alpha-beta pruning to play optimally. This means: +- Claude will never lose if it plays perfectly +- Best you can do is draw (if you play perfectly too) +- Good luck! ๐ŸŽฎ + +### API Endpoints (for Web UI) +- `POST /ttt/new` - Create new game +- `POST /ttt/{game_id}/move` - Make a move +- `GET /ttt/{game_id}/state` - Get game state +- `GET /ttt/games` - List all games + +--- + +## Troubleshooting + +### Claude doesn't have TTT tools +1. Make sure the Semsoon MCP server is running +2. Check Claude Desktop config includes Semsoon server +3. Restart Claude Desktop +4. Try: "What tools do you have available?" to verify + +### Game not responding +1. Check if MCP server is still running +2. Look for errors in server console +3. Try starting a new game + +### Can't find my game +- Games are stored in-memory, so they're lost if server restarts +- Use `ttt_list_games()` to see all active games + +--- + +## Tips for Playing + +1. **Center first** (position 4) is often a strong opening +2. **Watch for two in a row** - block Claude or complete your own line +3. **Control corners** - positions 0, 2, 6, 8 are strategic +4. **Think ahead** - Claude is using minimax, so play carefully! + +--- + +## Integration Details + +### File Structure +``` +backend/ +โ”œโ”€โ”€ mcp_server.py # Main Semsoon MCP server +โ”œโ”€โ”€ ttt_game.py # Game logic and AI +โ”œโ”€โ”€ demo_api.py # REST API endpoints +โ””โ”€โ”€ tools/ + โ””โ”€โ”€ ttt_tools.py # MCP tool registrations +``` + +### How It Works +1. You talk to Claude Desktop +2. Claude calls MCP tools (e.g., `ttt_make_move`) +3. Tools execute game logic in `ttt_game.py` +4. Results returned to Claude +5. Claude shows you the board and responds naturally + +--- + +## Have Fun! + +Enjoy playing Tic Tac Toe with Claude! It's a great way to: +- Take study breaks +- Test the MCP integration +- Challenge yourself against perfect AI play + +Remember: You're X, Claude is O. May the best player win! ๐Ÿ† diff --git a/USE_NGROK.md b/USE_NGROK.md new file mode 100644 index 0000000000000000000000000000000000000000..ff1b84ab5cfb7a7a5192ad9e03f9426095a99a14 --- /dev/null +++ b/USE_NGROK.md @@ -0,0 +1,27 @@ +# Using ngrok for HTTPS Access + +If you need an HTTPS endpoint for your MCP server: + +## 1. Install ngrok +Download from: https://ngrok.com/download + +## 2. Start your MCP server +```bash +cd backend +python mcp_server.py +``` + +## 3. Create HTTPS tunnel +```bash +ngrok http 8000 +``` + +This will give you a public HTTPS URL like: +`https://abc123.ngrok.io` + +## 4. Use the HTTPS URL +You can then use `https://abc123.ngrok.io/sse` as your MCP endpoint. + +--- + +**BUT REMEMBER**: Claude's web interface doesn't support custom MCP connectors. You need Claude Desktop for MCP integration! \ No newline at end of file diff --git a/app/api/code/execute/route.ts b/app/api/code/execute/route.ts new file mode 100644 index 0000000000000000000000000000000000000000..6d437812c21dd76df633fc1d06d08782f748dfcd --- /dev/null +++ b/app/api/code/execute/route.ts @@ -0,0 +1,186 @@ +import { NextRequest, NextResponse } from 'next/server' +import { exec } from 'child_process' +import { promisify } from 'util' +import fs from 'fs/promises' +import path from 'path' +import crypto from 'crypto' + +const execAsync = promisify(exec) + +// Session storage (in production, use a proper database) +const sessions = new Map() + +export async function POST(request: NextRequest) { + try { + const { sessionId, language, code, timestamp } = await request.json() + + if (!sessionId || !language || !code) { + return NextResponse.json( + { error: 'Missing required parameters' }, + { status: 400 } + ) + } + + // Create a temporary directory for this execution + const tempDir = path.join(process.cwd(), 'temp', sessionId) + await fs.mkdir(tempDir, { recursive: true }) + + let output = '' + let error = null + + try { + switch (language) { + case 'python': { + const fileName = path.join(tempDir, `script_${timestamp}.py`) + await fs.writeFile(fileName, code) + + try { + const result = await execAsync(`python "${fileName}"`, { + timeout: 10000, // 10 second timeout + maxBuffer: 1024 * 1024 // 1MB buffer + }) + output = result.stdout + if (result.stderr) { + error = result.stderr + } + } catch (execError: any) { + error = execError.message || 'Execution failed' + output = execError.stdout || '' + } + + // Clean up + await fs.unlink(fileName).catch(() => {}) + break + } + + case 'javascript': + case 'typescript': { + const fileName = path.join(tempDir, `script_${timestamp}.${language === 'typescript' ? 'ts' : 'js'}`) + await fs.writeFile(fileName, code) + + try { + const command = language === 'typescript' + ? `npx ts-node "${fileName}"` + : `node "${fileName}"` + + const result = await execAsync(command, { + timeout: 10000, + maxBuffer: 1024 * 1024 + }) + output = result.stdout + if (result.stderr) { + error = result.stderr + } + } catch (execError: any) { + error = execError.message || 'Execution failed' + output = execError.stdout || '' + } + + await fs.unlink(fileName).catch(() => {}) + break + } + + case 'react': + case 'flutter': { + // For React and Flutter, we'll return a message since they need build processes + output = `${language === 'react' ? 'React' : 'Flutter'} code saved successfully. +To run ${language} applications, use the integrated development server.` + + // Save the code for later use + const fileName = path.join(tempDir, `app_${timestamp}.${language === 'react' ? 'jsx' : 'dart'}`) + await fs.writeFile(fileName, code) + break + } + + case 'html': + case 'css': { + // HTML and CSS don't execute, just save them + const extension = language === 'html' ? 'html' : 'css' + const fileName = path.join(tempDir, `file_${timestamp}.${extension}`) + await fs.writeFile(fileName, code) + output = `${language.toUpperCase()} file saved successfully. Use the preview pane to see the result.` + break + } + + default: + error = `Unsupported language: ${language}` + } + + // Store in session + if (!sessions.has(sessionId)) { + sessions.set(sessionId, { + files: [], + executions: [] + }) + } + + const session = sessions.get(sessionId) + session.executions.push({ + language, + code, + output, + error, + timestamp + }) + + // Keep only last 50 executions + if (session.executions.length > 50) { + session.executions = session.executions.slice(-50) + } + + return NextResponse.json({ + output, + error, + timestamp, + sessionId + }) + + } catch (err: any) { + return NextResponse.json( + { error: err.message || 'Execution failed' }, + { status: 500 } + ) + } finally { + // Clean up temp directory after some time + setTimeout(async () => { + try { + await fs.rmdir(tempDir, { recursive: true }) + } catch {} + }, 60000) // Clean after 1 minute + } + + } catch (err: any) { + return NextResponse.json( + { error: err.message || 'Request failed' }, + { status: 500 } + ) + } +} + +export async function GET(request: NextRequest) { + const searchParams = request.nextUrl.searchParams + const sessionId = searchParams.get('sessionId') + + if (!sessionId) { + return NextResponse.json( + { error: 'Session ID required' }, + { status: 400 } + ) + } + + const session = sessions.get(sessionId) + + if (!session) { + return NextResponse.json({ + sessionId, + executions: [], + files: [] + }) + } + + return NextResponse.json({ + sessionId, + executions: session.executions || [], + files: session.files || [] + }) +} \ No newline at end of file diff --git a/app/api/code/public/route.ts b/app/api/code/public/route.ts new file mode 100644 index 0000000000000000000000000000000000000000..85dd72f027cdc91664c66e8435a5ebde017db8ad --- /dev/null +++ b/app/api/code/public/route.ts @@ -0,0 +1,85 @@ +import { NextRequest, NextResponse } from 'next/server' +import fs from 'fs/promises' +import path from 'path' + +const PUBLIC_DIR = path.join(process.cwd(), 'public', 'shared-code') + +// Initialize public directory +async function ensurePublicDir() { + await fs.mkdir(PUBLIC_DIR, { recursive: true }) +} + +export async function GET(request: NextRequest) { + try { + await ensurePublicDir() + + // Read all public files + const files = await fs.readdir(PUBLIC_DIR) + const publicFiles = [] + + for (const file of files) { + if (file.endsWith('.json')) { + try { + const content = await fs.readFile(path.join(PUBLIC_DIR, file), 'utf-8') + const fileData = JSON.parse(content) + publicFiles.push(fileData) + } catch (err) { + console.error(`Error reading file ${file}:`, err) + } + } + } + + // Sort by timestamp (newest first) + publicFiles.sort((a, b) => (b.timestamp || 0) - (a.timestamp || 0)) + + return NextResponse.json(publicFiles) + } catch (err: any) { + return NextResponse.json( + { error: 'Failed to load public files' }, + { status: 500 } + ) + } +} + +export async function POST(request: NextRequest) { + try { + const { sessionId, file } = await request.json() + + if (!sessionId || !file) { + return NextResponse.json( + { error: 'Missing required parameters' }, + { status: 400 } + ) + } + + await ensurePublicDir() + + // Add metadata + const publicFile = { + ...file, + id: `public_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`, + timestamp: Date.now(), + author: sessionId, + downloads: 0, + likes: 0 + } + + // Save to public directory + const fileName = `${publicFile.id}.json` + const filePath = path.join(PUBLIC_DIR, fileName) + + await fs.writeFile(filePath, JSON.stringify(publicFile, null, 2)) + + return NextResponse.json({ + success: true, + file: publicFile, + path: `/shared-code/${fileName}` + }) + + } catch (err: any) { + return NextResponse.json( + { error: err.message || 'Failed to save public file' }, + { status: 500 } + ) + } +} \ No newline at end of file diff --git a/app/api/code/save/route.ts b/app/api/code/save/route.ts new file mode 100644 index 0000000000000000000000000000000000000000..74a4f348982f04edfd799952298141d898cfb1b2 --- /dev/null +++ b/app/api/code/save/route.ts @@ -0,0 +1,130 @@ +import { NextRequest, NextResponse } from 'next/server' +import fs from 'fs' +import path from 'path' + +export async function POST(request: NextRequest) { + try { + const body = await request.json() + const { sessionId, code, timestamp } = body + + // Define the base path for saved code (accessible by MCP) + const baseDir = path.join(process.cwd(), 'data', 'vscode_sessions') + + // Create directory if it doesn't exist + if (!fs.existsSync(baseDir)) { + fs.mkdirSync(baseDir, { recursive: true }) + } + + // Create session directory + const sessionDir = path.join(baseDir, sessionId) + if (!fs.existsSync(sessionDir)) { + fs.mkdirSync(sessionDir, { recursive: true }) + } + + // Save each file + for (const file of code) { + const filePath = path.join(sessionDir, file.name) + fs.writeFileSync(filePath, file.content, 'utf-8') + } + + // Create metadata file + const metadata = { + sessionId, + timestamp, + files: code.map((f: any) => ({ + name: f.name, + language: f.language, + size: f.content.length + })), + created: new Date().toISOString() + } + + fs.writeFileSync( + path.join(sessionDir, 'metadata.json'), + JSON.stringify(metadata, null, 2), + 'utf-8' + ) + + return NextResponse.json({ + success: true, + message: 'Code saved successfully', + path: sessionDir, + sessionId + }) + } catch (error) { + console.error('Error saving code:', error) + return NextResponse.json( + { success: false, error: 'Failed to save code' }, + { status: 500 } + ) + } +} + +export async function GET(request: NextRequest) { + try { + const searchParams = request.nextUrl.searchParams + const sessionId = searchParams.get('sessionId') + + if (!sessionId) { + // List all sessions + const baseDir = path.join(process.cwd(), 'data', 'vscode_sessions') + + if (!fs.existsSync(baseDir)) { + return NextResponse.json({ sessions: [] }) + } + + const sessions = fs.readdirSync(baseDir) + .filter(dir => fs.statSync(path.join(baseDir, dir)).isDirectory()) + .map(dir => { + const metadataPath = path.join(baseDir, dir, 'metadata.json') + if (fs.existsSync(metadataPath)) { + const metadata = JSON.parse(fs.readFileSync(metadataPath, 'utf-8')) + return { + sessionId: dir, + ...metadata + } + } + return { sessionId: dir } + }) + + return NextResponse.json({ sessions }) + } + + // Get specific session + const sessionDir = path.join(process.cwd(), 'data', 'vscode_sessions', sessionId) + + if (!fs.existsSync(sessionDir)) { + return NextResponse.json( + { error: 'Session not found' }, + { status: 404 } + ) + } + + const files = fs.readdirSync(sessionDir) + .filter(file => file !== 'metadata.json') + .map(filename => { + const content = fs.readFileSync(path.join(sessionDir, filename), 'utf-8') + return { + name: filename, + content + } + }) + + const metadataPath = path.join(sessionDir, 'metadata.json') + const metadata = fs.existsSync(metadataPath) + ? JSON.parse(fs.readFileSync(metadataPath, 'utf-8')) + : {} + + return NextResponse.json({ + sessionId, + files, + metadata + }) + } catch (error) { + console.error('Error reading code:', error) + return NextResponse.json( + { error: 'Failed to read code' }, + { status: 500 } + ) + } +} \ No newline at end of file diff --git a/app/api/download/route.ts b/app/api/download/route.ts new file mode 100644 index 0000000000000000000000000000000000000000..b895377efc9a1cd68a0c3973904b00b7e1f04264 --- /dev/null +++ b/app/api/download/route.ts @@ -0,0 +1,99 @@ +import { NextRequest, NextResponse } from 'next/server' +import fs from 'fs' +import path from 'path' + +const DATA_DIR = path.join(process.cwd(), 'data') +const DOCS_DIR = path.join(DATA_DIR, 'documents') + +export async function GET(request: NextRequest) { + try { + const searchParams = request.nextUrl.searchParams + const filePath = searchParams.get('path') + const preview = searchParams.get('preview') === 'true' + + if (!filePath) { + return NextResponse.json({ error: 'File path required' }, { status: 400 }) + } + + const fullPath = path.join(DOCS_DIR, filePath) + + // Security check + if (!fullPath.startsWith(DOCS_DIR)) { + return NextResponse.json({ error: 'Invalid path' }, { status: 400 }) + } + + if (!fs.existsSync(fullPath)) { + return NextResponse.json({ error: 'File not found' }, { status: 404 }) + } + + const stats = fs.statSync(fullPath) + if (stats.isDirectory()) { + return NextResponse.json({ error: 'Cannot download directory' }, { status: 400 }) + } + + const fileBuffer = fs.readFileSync(fullPath) + const fileName = path.basename(filePath) + const ext = path.extname(fileName).toLowerCase() + + // Determine content type + let contentType = 'application/octet-stream' + const mimeTypes: Record = { + '.pdf': 'application/pdf', + '.doc': 'application/msword', + '.docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', + '.xls': 'application/vnd.ms-excel', + '.xlsx': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', + '.ppt': 'application/vnd.ms-powerpoint', + '.pptx': 'application/vnd.openxmlformats-officedocument.presentationml.presentation', + '.txt': 'text/plain', + '.md': 'text/markdown', + '.json': 'application/json', + '.html': 'text/html', + '.css': 'text/css', + '.js': 'text/javascript', + '.ts': 'text/typescript', + '.py': 'text/x-python', + '.java': 'text/x-java', + '.cpp': 'text/x-c++', + '.jpg': 'image/jpeg', + '.jpeg': 'image/jpeg', + '.png': 'image/png', + '.gif': 'image/gif', + '.svg': 'image/svg+xml', + '.mp3': 'audio/mpeg', + '.mp4': 'video/mp4', + '.zip': 'application/zip', + '.rar': 'application/x-rar-compressed' + } + + if (mimeTypes[ext]) { + contentType = mimeTypes[ext] + } + + const headers = new Headers({ + 'Content-Type': contentType, + 'Content-Length': fileBuffer.length.toString(), + }) + + // If not preview mode, add download header + if (!preview) { + headers.set('Content-Disposition', `attachment; filename="${fileName}"`) + } else { + // For preview, use inline disposition for supported types + if (['application/pdf', 'text/plain', 'text/markdown', 'application/json'].includes(contentType) || + contentType.startsWith('image/') || contentType.startsWith('text/')) { + headers.set('Content-Disposition', `inline; filename="${fileName}"`) + } else { + headers.set('Content-Disposition', `attachment; filename="${fileName}"`) + } + } + + return new NextResponse(fileBuffer, { headers }) + } catch (error) { + console.error('Error downloading file:', error) + return NextResponse.json( + { error: 'Failed to download file' }, + { status: 500 } + ) + } +} \ No newline at end of file diff --git a/app/api/files/route.ts b/app/api/files/route.ts new file mode 100644 index 0000000000000000000000000000000000000000..f827da5f8f31db61b014e9132a91beef5b45d3aa --- /dev/null +++ b/app/api/files/route.ts @@ -0,0 +1,183 @@ +import { NextRequest, NextResponse } from 'next/server' +import fs from 'fs' +import path from 'path' + +const DATA_DIR = path.join(process.cwd(), 'data') +const DOCS_DIR = path.join(DATA_DIR, 'documents') + +// Ensure directories exist +if (!fs.existsSync(DATA_DIR)) { + fs.mkdirSync(DATA_DIR, { recursive: true }) +} +if (!fs.existsSync(DOCS_DIR)) { + fs.mkdirSync(DOCS_DIR, { recursive: true }) +} + +interface FileItem { + name: string + type: 'file' | 'folder' + size?: number + modified?: string + path: string + extension?: string +} + +function getFileExtension(filename: string): string { + const ext = path.extname(filename).toLowerCase() + return ext.startsWith('.') ? ext.substring(1) : ext +} + +function getFilesRecursively(dir: string, basePath: string = ''): FileItem[] { + const files: FileItem[] = [] + + try { + const items = fs.readdirSync(dir) + + for (const item of items) { + // Skip hidden files and system files + if (item.startsWith('.') || item === 'exams.db') continue + + const fullPath = path.join(dir, item) + const relativePath = path.join(basePath, item).replace(/\\/g, '/') + const stats = fs.statSync(fullPath) + + if (stats.isDirectory()) { + files.push({ + name: item, + type: 'folder', + path: relativePath, + modified: stats.mtime.toISOString() + }) + // Recursively get files from subdirectories + const subFiles = getFilesRecursively(fullPath, relativePath) + files.push(...subFiles) + } else { + files.push({ + name: item, + type: 'file', + size: stats.size, + modified: stats.mtime.toISOString(), + path: relativePath, + extension: getFileExtension(item) + }) + } + } + } catch (error) { + console.error('Error reading directory:', error) + } + + return files +} + +export async function GET(request: NextRequest) { + const searchParams = request.nextUrl.searchParams + const folder = searchParams.get('folder') || '' + + try { + const targetDir = path.join(DOCS_DIR, folder) + + // Security check - prevent directory traversal + if (!targetDir.startsWith(DOCS_DIR)) { + return NextResponse.json({ error: 'Invalid path' }, { status: 400 }) + } + + if (!fs.existsSync(targetDir)) { + // If directory doesn't exist, create it + fs.mkdirSync(targetDir, { recursive: true }) + } + + const files = getFilesRecursively(targetDir, folder) + + return NextResponse.json({ + files, + currentPath: folder, + dataDir: DATA_DIR + }) + } catch (error) { + console.error('Error listing files:', error) + return NextResponse.json( + { error: 'Failed to list files' }, + { status: 500 } + ) + } +} + +// Create folder endpoint +export async function POST(request: NextRequest) { + try { + const body = await request.json() + const { folderName, parentPath = '' } = body + + if (!folderName) { + return NextResponse.json({ error: 'Folder name required' }, { status: 400 }) + } + + const folderPath = path.join(DOCS_DIR, parentPath, folderName) + + // Security check + if (!folderPath.startsWith(DOCS_DIR)) { + return NextResponse.json({ error: 'Invalid path' }, { status: 400 }) + } + + if (fs.existsSync(folderPath)) { + return NextResponse.json({ error: 'Folder already exists' }, { status: 400 }) + } + + fs.mkdirSync(folderPath, { recursive: true }) + + return NextResponse.json({ + success: true, + path: path.join(parentPath, folderName).replace(/\\/g, '/') + }) + } catch (error) { + console.error('Error creating folder:', error) + return NextResponse.json( + { error: 'Failed to create folder' }, + { status: 500 } + ) + } +} + +// Delete file endpoint +export async function DELETE(request: NextRequest) { + try { + const searchParams = request.nextUrl.searchParams + const filePath = searchParams.get('path') + + if (!filePath) { + return NextResponse.json({ error: 'File path required' }, { status: 400 }) + } + + const fullPath = path.join(DOCS_DIR, filePath) + + // Security check + if (!fullPath.startsWith(DOCS_DIR)) { + return NextResponse.json({ error: 'Invalid path' }, { status: 400 }) + } + + if (!fs.existsSync(fullPath)) { + return NextResponse.json({ error: 'File not found' }, { status: 404 }) + } + + // Move to trash instead of permanent delete + const trashDir = path.join(DATA_DIR, '.trash') + if (!fs.existsSync(trashDir)) { + fs.mkdirSync(trashDir, { recursive: true }) + } + + const timestamp = Date.now() + const trashPath = path.join(trashDir, `${timestamp}_${path.basename(filePath)}`) + fs.renameSync(fullPath, trashPath) + + return NextResponse.json({ + success: true, + message: 'File moved to trash' + }) + } catch (error) { + console.error('Error deleting file:', error) + return NextResponse.json( + { error: 'Failed to delete file' }, + { status: 500 } + ) + } +} \ No newline at end of file diff --git a/app/api/gemini/chat/route.ts b/app/api/gemini/chat/route.ts new file mode 100644 index 0000000000000000000000000000000000000000..7bf9f537a0939a2b404562a59974f7e76f597d16 --- /dev/null +++ b/app/api/gemini/chat/route.ts @@ -0,0 +1,113 @@ +import { NextRequest, NextResponse } from 'next/server' + +const GEMINI_API_URL = 'https://generativelanguage.googleapis.com/v1beta/models/gemini-1.5-flash:generateContent' +const GEMINI_API_KEY = process.env.GEMINI_API_KEY + +export async function POST(request: NextRequest) { + try { + const { message, imageUrl, history, generateImage } = await request.json() + + if (!GEMINI_API_KEY) { + return NextResponse.json( + { error: 'Gemini API key not configured on server. Please set GEMINI_API_KEY environment variable.' }, + { status: 500 } + ) + } + + // For image generation requests + if (generateImage) { + // Note: Gemini doesn't directly generate images, but we can use a prompt to describe what an image should contain + // You would typically integrate with an image generation service like Stable Diffusion or DALL-E here + return NextResponse.json({ + response: `I can describe the image for you: "${message}". For actual image generation, you would need to integrate with services like DALL-E or Stable Diffusion.`, + imageDescription: message, + note: 'Image generation requires integration with specialized services.' + }) + } + + // Prepare the request body for Gemini API + const requestBody: any = { + contents: [], + generationConfig: { + temperature: 0.9, + topK: 1, + topP: 1, + maxOutputTokens: 2048, + } + } + + // Add conversation history if available + if (history && history.length > 0) { + history.forEach((msg: any) => { + requestBody.contents.push({ + role: msg.role === 'user' ? 'user' : 'model', + parts: [{ text: msg.content }] + }) + }) + } + + // Prepare the current message + const parts: any[] = [] + + if (message) { + parts.push({ text: message }) + } + + if (imageUrl) { + // Extract base64 data from data URL + const base64Data = imageUrl.split(',')[1] + parts.push({ + inline_data: { + mime_type: imageUrl.split(':')[1].split(';')[0], + data: base64Data + } + }) + } + + requestBody.contents.push({ + role: 'user', + parts: parts + }) + + // Make request to Gemini API + const response = await fetch(`${GEMINI_API_URL}?key=${GEMINI_API_KEY}`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(requestBody) + }) + + if (!response.ok) { + const errorText = await response.text() + console.error('Gemini API error:', errorText) + return NextResponse.json( + { error: 'Failed to get response from Gemini', details: errorText }, + { status: response.status } + ) + } + + const data = await response.json() + + if (!data.candidates || data.candidates.length === 0) { + return NextResponse.json( + { error: 'No response generated' }, + { status: 500 } + ) + } + + const generatedText = data.candidates[0].content.parts[0].text + + return NextResponse.json({ + response: generatedText + }) + + } catch (error) { + console.error('Error in Gemini chat API:', error) + const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred' + return NextResponse.json( + { error: 'Failed to process request', details: errorMessage }, + { status: 500 } + ) + } +} \ No newline at end of file diff --git a/app/api/gemini/transcribe/route.ts b/app/api/gemini/transcribe/route.ts new file mode 100644 index 0000000000000000000000000000000000000000..57c06f26a0f2c3be94dc8ebb1bb651b77276d62a --- /dev/null +++ b/app/api/gemini/transcribe/route.ts @@ -0,0 +1,94 @@ +import { NextRequest, NextResponse } from 'next/server' + +// Note: For audio transcription, we'll use Gemini's multimodal capabilities +// In production, you might want to use Google Cloud Speech-to-Text API for better accuracy + +export async function POST(request: NextRequest) { + try { + const formData = await request.formData() + const audioFile = formData.get('audio') as File + const apiKey = formData.get('apiKey') as string + + if (!apiKey) { + return NextResponse.json( + { error: 'API key is required' }, + { status: 400 } + ) + } + + if (!audioFile) { + return NextResponse.json( + { error: 'Audio file is required' }, + { status: 400 } + ) + } + + // Convert audio file to base64 + const bytes = await audioFile.arrayBuffer() + const buffer = Buffer.from(bytes) + const base64Audio = buffer.toString('base64') + + // Use Gemini API to transcribe + // Note: Gemini 1.5 Pro supports audio, but Flash might have limitations + const GEMINI_API_URL = 'https://generativelanguage.googleapis.com/v1beta/models/gemini-1.5-flash:generateContent' + + const requestBody = { + contents: [{ + role: 'user', + parts: [ + { + text: 'Please transcribe the following audio accurately. Only return the transcription text, nothing else.' + }, + { + inline_data: { + mime_type: audioFile.type || 'audio/wav', + data: base64Audio + } + } + ] + }], + generationConfig: { + temperature: 0.1, + topK: 1, + topP: 1, + maxOutputTokens: 2048, + } + } + + const response = await fetch(`${GEMINI_API_URL}?key=${apiKey}`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(requestBody) + }) + + if (!response.ok) { + const error = await response.json() + + // If Gemini doesn't support audio, provide alternative solution + if (error.error?.message?.includes('audio') || error.error?.message?.includes('unsupported')) { + return NextResponse.json({ + transcription: '[Audio transcription requires Gemini 1.5 Pro or Google Cloud Speech-to-Text API. Please upgrade your API access or use the chat feature with text input.]', + warning: 'Audio transcription not fully supported with current model' + }) + } + + throw new Error(error.error?.message || 'Failed to transcribe audio') + } + + const data = await response.json() + const transcription = data.candidates?.[0]?.content?.parts?.[0]?.text || 'Could not transcribe audio' + + return NextResponse.json({ transcription }) + + } catch (error) { + console.error('Transcription error:', error) + + // Provide a helpful fallback message + return NextResponse.json({ + transcription: '', + error: error instanceof Error ? error.message : 'Transcription failed. Note: Audio transcription requires Gemini 1.5 Pro or a dedicated speech-to-text API.' + }, { status: 500 }) + } +} \ No newline at end of file diff --git a/app/api/public/route.ts b/app/api/public/route.ts new file mode 100644 index 0000000000000000000000000000000000000000..ed5f7ac5629b96597c6ac39a8707341704459bbe --- /dev/null +++ b/app/api/public/route.ts @@ -0,0 +1,178 @@ +import { NextRequest, NextResponse } from 'next/server' +import fs from 'fs' +import path from 'path' + +const DATA_DIR = path.join(process.cwd(), 'data') +const PUBLIC_DIR = path.join(DATA_DIR, 'public') + +// Ensure public directory exists +if (!fs.existsSync(PUBLIC_DIR)) { + fs.mkdirSync(PUBLIC_DIR, { recursive: true }) +} + +interface FileItem { + name: string + type: 'file' | 'folder' + size?: number + modified?: string + path: string + extension?: string + uploadedBy?: string + uploadedAt?: string +} + +function getFileExtension(filename: string): string { + const ext = path.extname(filename).toLowerCase() + return ext.startsWith('.') ? ext.substring(1) : ext +} + +export async function GET(request: NextRequest) { + const searchParams = request.nextUrl.searchParams + const folder = searchParams.get('folder') || '' + + try { + const targetDir = path.join(PUBLIC_DIR, folder) + + // Security check - prevent directory traversal + if (!targetDir.startsWith(PUBLIC_DIR)) { + return NextResponse.json({ error: 'Invalid path' }, { status: 400 }) + } + + if (!fs.existsSync(targetDir)) { + fs.mkdirSync(targetDir, { recursive: true }) + } + + const files: FileItem[] = [] + const items = fs.readdirSync(targetDir) + + for (const item of items) { + // Skip hidden files + if (item.startsWith('.')) continue + + const fullPath = path.join(targetDir, item) + const relativePath = path.join(folder, item).replace(/\\/g, '/') + const stats = fs.statSync(fullPath) + + if (stats.isDirectory()) { + files.push({ + name: item, + type: 'folder', + path: relativePath, + modified: stats.mtime.toISOString() + }) + } else { + // Try to read metadata if it exists + const metadataPath = fullPath + '.meta.json' + let metadata: any = {} + if (fs.existsSync(metadataPath)) { + try { + metadata = JSON.parse(fs.readFileSync(metadataPath, 'utf-8')) + } catch (e) { + // Ignore metadata errors + } + } + + files.push({ + name: item, + type: 'file', + size: stats.size, + modified: stats.mtime.toISOString(), + path: relativePath, + extension: getFileExtension(item), + uploadedBy: metadata.uploadedBy || 'Anonymous', + uploadedAt: metadata.uploadedAt || stats.birthtime.toISOString() + }) + } + } + + return NextResponse.json({ + files, + currentPath: folder, + isPublic: true + }) + } catch (error) { + console.error('Error listing public files:', error) + return NextResponse.json( + { error: 'Failed to list public files' }, + { status: 500 } + ) + } +} + +// Upload to public folder +export async function POST(request: NextRequest) { + try { + const formData = await request.formData() + const file = formData.get('file') as File + const folder = formData.get('folder') as string || '' + const uploadedBy = formData.get('uploadedBy') as string || 'Anonymous' + + if (!file) { + return NextResponse.json({ error: 'No file provided' }, { status: 400 }) + } + + const targetDir = path.join(PUBLIC_DIR, folder) + + // Security check + if (!targetDir.startsWith(PUBLIC_DIR)) { + return NextResponse.json({ error: 'Invalid path' }, { status: 400 }) + } + + if (!fs.existsSync(targetDir)) { + fs.mkdirSync(targetDir, { recursive: true }) + } + + const fileName = file.name + const filePath = path.join(targetDir, fileName) + + // Check if file already exists + if (fs.existsSync(filePath)) { + // Add timestamp to filename + const timestamp = Date.now() + const ext = path.extname(fileName) + const baseName = path.basename(fileName, ext) + const newFileName = `${baseName}_${timestamp}${ext}` + const newFilePath = path.join(targetDir, newFileName) + + const buffer = Buffer.from(await file.arrayBuffer()) + fs.writeFileSync(newFilePath, buffer) + + // Save metadata + const metadataPath = newFilePath + '.meta.json' + fs.writeFileSync(metadataPath, JSON.stringify({ + uploadedBy, + uploadedAt: new Date().toISOString(), + originalName: fileName + })) + + return NextResponse.json({ + success: true, + message: 'File uploaded to public folder', + path: path.join(folder, newFileName).replace(/\\/g, '/'), + renamed: true + }) + } else { + const buffer = Buffer.from(await file.arrayBuffer()) + fs.writeFileSync(filePath, buffer) + + // Save metadata + const metadataPath = filePath + '.meta.json' + fs.writeFileSync(metadataPath, JSON.stringify({ + uploadedBy, + uploadedAt: new Date().toISOString() + })) + + return NextResponse.json({ + success: true, + message: 'File uploaded to public folder', + path: path.join(folder, fileName).replace(/\\/g, '/') + }) + } + } catch (error) { + console.error('Error uploading to public folder:', error) + return NextResponse.json( + { error: 'Failed to upload file' }, + { status: 500 } + ) + } +} \ No newline at end of file diff --git a/app/api/upload/route.ts b/app/api/upload/route.ts new file mode 100644 index 0000000000000000000000000000000000000000..1ec85c63c2bd96ae9eb94613b9fbb02a15f2602d --- /dev/null +++ b/app/api/upload/route.ts @@ -0,0 +1,84 @@ +import { NextRequest, NextResponse } from 'next/server' +import fs from 'fs' +import path from 'path' + +const DATA_DIR = path.join(process.cwd(), 'data') +const DOCS_DIR = path.join(DATA_DIR, 'documents') +const MAX_FILE_SIZE = 50 * 1024 * 1024 // 50MB + +export async function POST(request: NextRequest) { + try { + const formData = await request.formData() + const file = formData.get('file') as File + const folderPath = formData.get('folder') as string || '' + + if (!file) { + return NextResponse.json({ error: 'No file provided' }, { status: 400 }) + } + + // Check file size + if (file.size > MAX_FILE_SIZE) { + return NextResponse.json( + { error: `File size exceeds ${MAX_FILE_SIZE / 1024 / 1024}MB limit` }, + { status: 400 } + ) + } + + // Sanitize filename + const fileName = file.name.replace(/[^a-zA-Z0-9._-]/g, '_') + const uploadDir = path.join(DOCS_DIR, folderPath) + const filePath = path.join(uploadDir, fileName) + + // Security check + if (!filePath.startsWith(DOCS_DIR)) { + return NextResponse.json({ error: 'Invalid upload path' }, { status: 400 }) + } + + // Create directory if it doesn't exist + if (!fs.existsSync(uploadDir)) { + fs.mkdirSync(uploadDir, { recursive: true }) + } + + // Check if file already exists + if (fs.existsSync(filePath)) { + // Add timestamp to filename if it exists + const timestamp = Date.now() + const ext = path.extname(fileName) + const baseName = path.basename(fileName, ext) + const newFileName = `${baseName}_${timestamp}${ext}` + const newFilePath = path.join(uploadDir, newFileName) + + // Convert file to buffer and save + const bytes = await file.arrayBuffer() + const buffer = Buffer.from(bytes) + fs.writeFileSync(newFilePath, buffer) + + return NextResponse.json({ + success: true, + message: 'File uploaded (renamed due to conflict)', + fileName: newFileName, + path: path.join(folderPath, newFileName).replace(/\\/g, '/'), + size: file.size + }) + } + + // Convert file to buffer and save + const bytes = await file.arrayBuffer() + const buffer = Buffer.from(bytes) + fs.writeFileSync(filePath, buffer) + + return NextResponse.json({ + success: true, + message: 'File uploaded successfully', + fileName: fileName, + path: path.join(folderPath, fileName).replace(/\\/g, '/'), + size: file.size + }) + } catch (error) { + console.error('Error uploading file:', error) + return NextResponse.json( + { error: 'Failed to upload file' }, + { status: 500 } + ) + } +} \ No newline at end of file diff --git a/app/components/AboutModal.tsx b/app/components/AboutModal.tsx new file mode 100644 index 0000000000000000000000000000000000000000..4455f6de0c21a04f122f570470c177a4ab749e67 --- /dev/null +++ b/app/components/AboutModal.tsx @@ -0,0 +1,179 @@ +'use client' + +import React, { useEffect, useState } from 'react' +import { X, Info, HardDrive, Cpu, Globe, Key } from '@phosphor-icons/react' +import { motion, AnimatePresence } from 'framer-motion' + +interface AboutModalProps { + isOpen: boolean + onClose: () => void +} + +export function AboutModal({ isOpen, onClose }: AboutModalProps) { + const [systemId, setSystemId] = useState('') + const [publicPath, setPublicPath] = useState('') + const [tempPath, setTempPath] = useState('') + + useEffect(() => { + // Generate unique system ID based on browser fingerprint + const generateSystemId = () => { + const nav = window.navigator + const screen = window.screen + const fingerprint = [ + nav.userAgent, + nav.language, + screen.colorDepth, + screen.width, + screen.height, + new Date().getTimezoneOffset(), + nav.hardwareConcurrency, + nav.platform + ].join('|') + + // Create a simple hash + let hash = 0 + for (let i = 0; i < fingerprint.length; i++) { + const char = fingerprint.charCodeAt(i) + hash = ((hash << 5) - hash) + char + hash = hash & hash // Convert to 32-bit integer + } + + // Convert to hex and make it look like a system ID + const hexHash = Math.abs(hash).toString(16).toUpperCase() + return `REUBEN-${hexHash.substring(0, 4)}-${hexHash.substring(4, 8)}-${Date.now().toString(36).toUpperCase()}` + } + + // Set system paths + setSystemId(generateSystemId()) + setPublicPath('E:\\mpc-hackathon\\public') + setTempPath(`E:\\mpc-hackathon\\data\\temp_${Date.now()}`) + }, []) + + if (!isOpen) return null + + return ( + + {isOpen && ( + <> +
+ +
+ {/* Logo and Title */} +
+
+ R +
+

Reuben OS

+

Advanced Desktop Environment

+

Version 1.0.0

+
+ + {/* System Information */} +
+
+
+ + System ID +
+ + {systemId || 'Generating...'} + +
+ +
+
+ + Storage Paths +
+
+
+ Public: + + {publicPath} + +
+
+ Temp: + + {tempPath} + +
+
+
+ +
+
+ + Capabilities +
+
+
+ + Python Code Execution (via MCP) +
+
+ + HTML/CSS/JS Preview +
+
+ + File Management System +
+
+ + Claude AI Integration +
+
+ + Flutter/Dart (Coming Soon) +
+
+
+ +
+
+ + MCP Server +
+
+
Status: Connected
+
Endpoint: reuben-os
+
Tools: 25+ available
+
+
+
+ + {/* Footer */} +
+

ยฉ 2024 Reuben OS. All rights reserved.

+

Built with Next.js, React, and MCP Technology

+
+ + {/* Close Button */} + + + {/* X button in corner */} + +
+
+ + )} + + ) +} \ No newline at end of file diff --git a/app/components/BackgroundSelector.tsx b/app/components/BackgroundSelector.tsx new file mode 100644 index 0000000000000000000000000000000000000000..9dea81d5fb12d8a975d735282e3c8702d664a867 --- /dev/null +++ b/app/components/BackgroundSelector.tsx @@ -0,0 +1,217 @@ +'use client' + +import React, { useState, useRef } from 'react' +import { motion, AnimatePresence } from 'framer-motion' +import { X, Upload, Image, Check } from '@phosphor-icons/react' + +interface BackgroundSelectorProps { + isOpen: boolean + onClose: () => void + onSelectBackground: (background: string | File) => void + currentBackground: string +} + +const presetBackgrounds = [ + { + id: 'gradient-purple', + name: 'Ubuntu Purple', + style: 'linear-gradient(135deg, #77216F 0%, #5E2750 50%, #2C001E 100%)' + }, + { + id: 'gradient-blue', + name: 'Ocean Blue', + style: 'linear-gradient(135deg, #1e3c72 0%, #2a5298 50%, #7e8ba3 100%)' + }, + { + id: 'gradient-green', + name: 'Forest Green', + style: 'linear-gradient(135deg, #134e5e 0%, #71b280 50%, #a8e063 100%)' + }, + { + id: 'gradient-orange', + name: 'Sunset Orange', + style: 'linear-gradient(135deg, #ff512f 0%, #dd2476 50%, #f09819 100%)' + }, + { + id: 'gradient-dark', + name: 'Dark Mode', + style: 'linear-gradient(135deg, #0f0f0f 0%, #1a1a1a 50%, #2d2d2d 100%)' + }, + { + id: 'gradient-cosmic', + name: 'Cosmic', + style: 'linear-gradient(135deg, #667eea 0%, #764ba2 50%, #f093fb 100%)' + } +] + +export function BackgroundSelector({ + isOpen, + onClose, + onSelectBackground, + currentBackground +}: BackgroundSelectorProps) { + const [selectedTab, setSelectedTab] = useState<'presets' | 'upload'>('presets') + const [uploadedImage, setUploadedImage] = useState(null) + const fileInputRef = useRef(null) + + const handleFileUpload = (event: React.ChangeEvent) => { + const file = event.target.files?.[0] + if (file) { + const reader = new FileReader() + reader.onload = (e) => { + const result = e.target?.result as string + setUploadedImage(result) + onSelectBackground(file) + } + reader.readAsDataURL(file) + } + } + + return ( + + {isOpen && ( + <> + {/* Backdrop */} + + + {/* Modal */} + + {/* Header */} +
+

Change Desktop Background

+ +
+ + {/* Tabs */} +
+ + +
+ + {/* Content */} +
+ {selectedTab === 'presets' ? ( +
+ {presetBackgrounds.map((bg) => ( + + ))} +
+ ) : ( +
+ + + {uploadedImage ? ( +
+ Uploaded background + +
+ ) : ( + <> +
fileInputRef.current?.click()} + className="w-32 h-32 border-2 border-dashed border-white/30 rounded-lg flex flex-col items-center justify-center cursor-pointer hover:border-white/50 transition-colors" + > + + Click to upload +
+

+ Supported formats: JPG, PNG, WEBP, GIF +

+ + )} +
+ )} +
+ + {/* Footer */} +
+ + +
+
+ + )} +
+ ) +} \ No newline at end of file diff --git a/app/components/Calendar.tsx b/app/components/Calendar.tsx new file mode 100644 index 0000000000000000000000000000000000000000..c3741c4db7a5008cfe3bf48eca9235d5e7045ccd --- /dev/null +++ b/app/components/Calendar.tsx @@ -0,0 +1,301 @@ +'use client' + +import React, { useState } from 'react' +import { X, Minus, Square, CaretLeft, CaretRight } from '@phosphor-icons/react' +import { motion } from 'framer-motion' +import { useKV } from '../hooks/useKV' + +interface CalendarProps { + onClose: () => void +} + +interface Event { + id: string + date: string + title: string + type: 'holiday' | 'custom' +} + +const defaultHolidays: Event[] = [ + { id: '1', date: '2025-01-01', title: 'New Year\'s Day', type: 'holiday' }, + { id: '2', date: '2025-02-14', title: 'Valentine\'s Day', type: 'holiday' }, + { id: '3', date: '2025-03-17', title: 'St. Patrick\'s Day', type: 'holiday' }, + { id: '4', date: '2025-04-20', title: 'Easter Sunday', type: 'holiday' }, + { id: '5', date: '2025-05-11', title: 'Mother\'s Day', type: 'holiday' }, + { id: '6', date: '2025-06-15', title: 'Father\'s Day', type: 'holiday' }, + { id: '7', date: '2025-07-04', title: 'Independence Day', type: 'holiday' }, + { id: '8', date: '2025-10-31', title: 'Halloween', type: 'holiday' }, + { id: '9', date: '2025-11-27', title: 'Thanksgiving', type: 'holiday' }, + { id: '10', date: '2025-12-25', title: 'Christmas Day', type: 'holiday' }, + { id: '11', date: '2025-12-31', title: 'New Year\'s Eve', type: 'holiday' }, +] + +export function Calendar({ onClose }: CalendarProps) { + const [currentDate, setCurrentDate] = useState(new Date()) + const [windowPos, setWindowPos] = useState({ x: 100, y: 100 }) + const [isDragging, setIsDragging] = useState(false) + const [dragStart, setDragStart] = useState({ x: 0, y: 0 }) + const [events, setEvents] = useKV('calendar-events', defaultHolidays) + const [selectedDay, setSelectedDay] = useState(null) + + const handleMouseDown = (e: React.MouseEvent) => { + if ((e.target as HTMLElement).closest('.window-controls')) return + setIsDragging(true) + setDragStart({ x: e.clientX - windowPos.x, y: e.clientY - windowPos.y }) + } + + const handleMouseMove = (e: MouseEvent) => { + if (isDragging) { + setWindowPos({ + x: e.clientX - dragStart.x, + y: e.clientY - dragStart.y, + }) + } + } + + const handleMouseUp = () => { + setIsDragging(false) + } + + React.useEffect(() => { + if (isDragging) { + window.addEventListener('mousemove', handleMouseMove) + window.addEventListener('mouseup', handleMouseUp) + return () => { + window.removeEventListener('mousemove', handleMouseMove) + window.removeEventListener('mouseup', handleMouseUp) + } + } + }, [isDragging, dragStart]) + + const monthNames = [ + 'January', 'February', 'March', 'April', 'May', 'June', + 'July', 'August', 'September', 'October', 'November', 'December' + ] + + const daysOfWeek = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'] + + const getDaysInMonth = (date: Date) => { + const year = date.getFullYear() + const month = date.getMonth() + const firstDay = new Date(year, month, 1) + const lastDay = new Date(year, month + 1, 0) + const daysInMonth = lastDay.getDate() + const startingDayOfWeek = firstDay.getDay() + + const days: (number | null)[] = [] + for (let i = 0; i < startingDayOfWeek; i++) { + days.push(null) + } + for (let i = 1; i <= daysInMonth; i++) { + days.push(i) + } + return days + } + + const days = getDaysInMonth(currentDate) + const today = new Date() + const isToday = (day: number | null) => { + if (!day) return false + return ( + day === today.getDate() && + currentDate.getMonth() === today.getMonth() && + currentDate.getFullYear() === today.getFullYear() + ) + } + + const getEventsForDay = (day: number | null) => { + if (!day || !events) return [] + const dateStr = `${currentDate.getFullYear()}-${String(currentDate.getMonth() + 1).padStart(2, '0')}-${String(day).padStart(2, '0')}` + return events.filter(event => event.date === dateStr) + } + + const hasEvent = (day: number | null) => { + return getEventsForDay(day).length > 0 + } + + const previousMonth = () => { + setCurrentDate(new Date(currentDate.getFullYear(), currentDate.getMonth() - 1)) + setSelectedDay(null) + } + + const nextMonth = () => { + setCurrentDate(new Date(currentDate.getFullYear(), currentDate.getMonth() + 1)) + setSelectedDay(null) + } + + const handleDayClick = (day: number | null) => { + if (day) { + setSelectedDay(day) + } + } + + const selectedDayEvents = selectedDay ? getEventsForDay(selectedDay) : [] + + return ( + +
+
+
+ + + +
+ Calendar +
+
+ +
+
+ +
+ {monthNames[currentDate.getMonth()]} {currentDate.getFullYear()} +
+ +
+ +
+ {daysOfWeek.map((day) => ( +
+ {day} +
+ ))} +
+ +
+ {days.map((day, index) => ( + day ? ( + + ) : ( +
+ ) + ))} +
+ + {selectedDayEvents.length > 0 && ( +
+
+ {monthNames[currentDate.getMonth()]} {selectedDay}, {currentDate.getFullYear()} +
+
+ {selectedDayEvents.map((event) => ( +
+
+
+
+ {event.title} +
+
+ {event.type === 'holiday' ? 'Holiday' : 'Event'} +
+
+
+ ))} +
+
+ )} + + {!selectedDay && ( +
+
Today
+
+ {today.toLocaleDateString('en-US', { + weekday: 'long', + year: 'numeric', + month: 'long', + day: 'numeric' + })} +
+ {getEventsForDay(today.getDate()).length > 0 && ( +
+ {getEventsForDay(today.getDate()).map((event) => ( +
+
+
+
+ {event.title} +
+
+
+ ))} +
+ )} +
+ )} +
+ + ) +} diff --git a/app/components/ClaudeIntegration.tsx b/app/components/ClaudeIntegration.tsx new file mode 100644 index 0000000000000000000000000000000000000000..31ebf891365efe0019885b09e5b7a66010e40ce3 --- /dev/null +++ b/app/components/ClaudeIntegration.tsx @@ -0,0 +1,240 @@ +'use client' + +import React, { useState } from 'react' +import { motion } from 'framer-motion' +import { + Upload, + Robot, + FileText, + CheckCircle, + Warning, + Info, + Copy, + Download +} from '@phosphor-icons/react' + +interface ClaudeIntegrationProps { + file: File | null + onClose: () => void +} + +export function ClaudeIntegration({ file, onClose }: ClaudeIntegrationProps) { + const [uploadMethod, setUploadMethod] = useState<'direct' | 'chunked' | 'reference'>('direct') + const [processing, setProcessing] = useState(false) + const [result, setResult] = useState('') + + const handleProcess = async () => { + if (!file) return + + setProcessing(true) + + try { + if (uploadMethod === 'direct') { + // Direct upload - best for small files < 1MB + const content = await file.text() + setResult(`File: ${file.name}\n\nContent:\n${content}`) + } else if (uploadMethod === 'chunked') { + // Chunked upload - for larger files, split into manageable chunks + const CHUNK_SIZE = 50000 // 50KB chunks + const content = await file.text() + const chunks = [] + + for (let i = 0; i < content.length; i += CHUNK_SIZE) { + chunks.push(content.slice(i, i + CHUNK_SIZE)) + } + + setResult( + `File: ${file.name}\n` + + `Total chunks: ${chunks.length}\n` + + `Chunk size: ${CHUNK_SIZE} characters\n\n` + + `Instructions for Claude:\n` + + `This file has been split into ${chunks.length} chunks.\n` + + `You can process each chunk sequentially.\n\n` + + `First chunk:\n${chunks[0].slice(0, 500)}...` + ) + } else { + // Reference method - store file and provide reference + setResult( + `File Reference Created:\n` + + `Name: ${file.name}\n` + + `Size: ${formatFileSize(file.size)}\n` + + `Type: ${file.type}\n\n` + + `This file has been stored in the system.\n` + + `You can reference it by name in your conversation with Claude.\n` + + `Claude can access and analyze the file content when needed.` + ) + } + } catch (error) { + console.error('Error processing file:', error) + setResult('Error processing file') + } finally { + setProcessing(false) + } + } + + const formatFileSize = (bytes: number) => { + const units = ['B', 'KB', 'MB', 'GB'] + let size = bytes + let unitIndex = 0 + + while (size >= 1024 && unitIndex < units.length - 1) { + size /= 1024 + unitIndex++ + } + + return `${size.toFixed(2)} ${units[unitIndex]}` + } + + const copyToClipboard = () => { + navigator.clipboard.writeText(result) + } + + return ( + + e.stopPropagation()} + > + {/* Header */} +
+
+ +

Claude Integration

+
+

+ Optimize file upload for Claude to preserve context +

+
+ + {/* Content */} +
+ {file && ( +
+
+ +
+

{file.name}

+

{formatFileSize(file.size)}

+
+
+
+ )} + + {/* Upload Methods */} +
+

+ Upload Method +

+ + + + + + +
+ + {/* Info Box */} +
+ +
+

Context Preservation Tips:

+
    +
  • โ€ข Use direct upload for code files and short documents
  • +
  • โ€ข Use chunked upload for large documents or datasets
  • +
  • โ€ข Use reference method for binary files or archives
  • +
+
+
+ + {/* Result */} + {result && ( +
+
+
+ + Ready for Claude +
+ +
+
+                {result}
+              
+
+ )} +
+ + {/* Footer */} +
+ + +
+
+
+ ) +} \ No newline at end of file diff --git a/app/components/Clock.tsx b/app/components/Clock.tsx new file mode 100644 index 0000000000000000000000000000000000000000..65e82c46840e1142bc8c636e40619de995cb1c14 --- /dev/null +++ b/app/components/Clock.tsx @@ -0,0 +1,188 @@ +'use client' + +import React, { useState, useEffect } from 'react' +import Window from './Window' + +interface ClockProps { + onClose: () => void +} + +export function Clock({ onClose }: ClockProps) { + const [time, setTime] = useState(new Date()) + const [viewMode, setViewMode] = useState<'analog' | 'digital' | 'world'>('analog') + + useEffect(() => { + const timer = setInterval(() => { + setTime(new Date()) + }, 1000) + + return () => clearInterval(timer) + }, []) + + // Calculate rotation angles for clock hands + const seconds = time.getSeconds() + const minutes = time.getMinutes() + const hours = time.getHours() + + const secondDegrees = (seconds / 60) * 360 - 90 + const minuteDegrees = ((minutes / 60) * 360) + ((seconds / 60) * 6) - 90 + const hourDegrees = ((hours % 12 / 12) * 360) + ((minutes / 60) * 30) - 90 + + const worldClocks = [ + { city: 'New York', offset: -5 }, + { city: 'London', offset: 0 }, + { city: 'Tokyo', offset: 9 }, + { city: 'Sydney', offset: 11 } + ] + + const getWorldTime = (offset: number) => { + const utc = time.getTime() + (time.getTimezoneOffset() * 60000) + const cityTime = new Date(utc + (3600000 * offset)) + return cityTime.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit' }) + } + + return ( + +
+ {/* Tab Bar */} +
+ + + +
+ + {/* Clock Display */} +
+ {viewMode === 'analog' && ( +
+
+ {/* Clock Face */} +
+ {/* Hour Markers */} + {[...Array(12)].map((_, i) => ( +
+
+
+ ))} + + {/* Clock Hands */} +
+
+
+ + {/* Center Dot */} +
+
+
+ + {/* Digital Time Below Analog Clock */} +
+
+ {time.toLocaleTimeString()} +
+
+ {time.toLocaleDateString('en-US', { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' })} +
+
+
+ )} + + {viewMode === 'digital' && ( +
+
+ {time.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', second: '2-digit' })} +
+
+ {time.toLocaleDateString('en-US', { weekday: 'long' })} +
+
+ {time.toLocaleDateString('en-US', { year: 'numeric', month: 'long', day: 'numeric' })} +
+
+ )} + + {viewMode === 'world' && ( +
+
+ {worldClocks.map((clock) => ( +
+
+
{clock.city}
+
UTC{clock.offset >= 0 ? '+' : ''}{clock.offset}
+
+
+ {getWorldTime(clock.offset)} +
+
+ ))} +
+
+ )} +
+
+ + ) +} \ No newline at end of file diff --git a/app/components/CodeExecutor.tsx b/app/components/CodeExecutor.tsx new file mode 100644 index 0000000000000000000000000000000000000000..957b31d1ff33a6a22a43d44756b84044eb851fb0 --- /dev/null +++ b/app/components/CodeExecutor.tsx @@ -0,0 +1,306 @@ +'use client' + +import React, { useState } from 'react' +import Window from './Window' +import Editor from '@monaco-editor/react' +import { + Play, + Code, + FileText, + Download, + Copy, + CheckCircle, + Warning, + CloudArrowUp +} from '@phosphor-icons/react' + +interface CodeExecutorProps { + onClose: () => void + initialCode?: string + language?: string +} + +export function CodeExecutor({ onClose, initialCode = '', language = 'python' }: CodeExecutorProps) { + const [code, setCode] = useState(initialCode || `# Reuben OS Code Executor +# Write your Python code here and click Run to execute + +import matplotlib.pyplot as plt +import numpy as np + +# Create sample data +x = np.linspace(0, 10, 100) +y = np.sin(x) + +# Create plot +plt.figure(figsize=(10, 6)) +plt.plot(x, y, 'b-', linewidth=2, label='sin(x)') +plt.grid(True, alpha=0.3) +plt.xlabel('X axis') +plt.ylabel('Y axis') +plt.title('Sample Plot in Reuben OS') +plt.legend() + +# This will automatically save and display the plot +plt.show() + +print("Plot generated successfully!") +print(f"X range: {x[0]:.2f} to {x[-1]:.2f}") +print(f"Y range: {y.min():.2f} to {y.max():.2f}")`) + + const [output, setOutput] = useState('') + const [isRunning, setIsRunning] = useState(false) + const [error, setError] = useState('') + const [plotPath, setPlotPath] = useState('') + const [selectedLanguage, setSelectedLanguage] = useState(language) + + const executeCode = async () => { + setIsRunning(true) + setOutput('') + setError('') + setPlotPath('') + + try { + // Save code to file system first + const sessionId = `exec_${Date.now()}_${Math.random().toString(36).substr(2, 9)}` + const saveResponse = await fetch('/api/code/save', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + sessionId, + code: [{ + name: `script.${selectedLanguage === 'python' ? 'py' : selectedLanguage === 'javascript' ? 'js' : 'html'}`, + language: selectedLanguage, + content: code + }], + timestamp: Date.now() + }) + }) + + if (!saveResponse.ok) { + throw new Error('Failed to save code') + } + + // Execute based on language + if (selectedLanguage === 'python') { + // For Python, we need MCP to execute it + setOutput('Python code saved! Use MCP tools to execute:\n') + setOutput(prev => prev + `\nSession ID: ${sessionId}\n`) + setOutput(prev => prev + `\nFile saved to: data/vscode_sessions/${sessionId}/script.py\n`) + setOutput(prev => prev + '\n๐Ÿ“ Note: To execute Python code, use the MCP execute_python_code tool from Claude Desktop.') + + // Show sample MCP command + setOutput(prev => prev + '\n\n๐Ÿ’ก MCP Command Example:\n') + setOutput(prev => prev + 'execute_python_code(code=, save_output=true)\n') + + // If it's matplotlib code, suggest using execute_matplotlib_code + if (code.includes('matplotlib') || code.includes('plt.')) { + setOutput(prev => prev + '\n๐ŸŽจ For matplotlib plots, use:\n') + setOutput(prev => prev + 'execute_matplotlib_code(code=, output_format="png")') + } + } else if (selectedLanguage === 'html' || selectedLanguage === 'javascript') { + // For HTML/JS, we can execute in browser + executeWebCode() + } + } catch (err) { + setError(err instanceof Error ? err.message : 'Unknown error occurred') + } finally { + setIsRunning(false) + } + } + + const executeWebCode = () => { + if (selectedLanguage === 'html') { + // Create iframe for HTML preview + const iframe = document.createElement('iframe') + iframe.style.width = '100%' + iframe.style.height = '100%' + iframe.style.border = 'none' + iframe.srcdoc = code + + const outputElement = document.getElementById('code-output') + if (outputElement) { + outputElement.innerHTML = '' + outputElement.appendChild(iframe) + } + setOutput('HTML rendered in preview pane') + } else if (selectedLanguage === 'javascript') { + // Execute JavaScript in sandboxed environment + try { + // Capture console.log outputs + const logs: string[] = [] + const originalLog = console.log + console.log = (...args) => { + logs.push(args.map(arg => String(arg)).join(' ')) + } + + // Execute the code + const func = new Function(code) + const result = func() + + // Restore console.log + console.log = originalLog + + // Display output + let outputText = logs.join('\n') + if (result !== undefined) { + outputText += `\n\nReturn value: ${JSON.stringify(result, null, 2)}` + } + setOutput(outputText || 'Code executed successfully (no output)') + } catch (err) { + setError(err instanceof Error ? err.message : 'JavaScript execution error') + } + } + } + + const copyOutput = () => { + navigator.clipboard.writeText(output || error) + + // Show copied feedback + const copiedDiv = document.createElement('div') + copiedDiv.className = 'fixed bottom-4 right-4 bg-blue-600 text-white px-4 py-2 rounded-lg shadow-lg z-[200] flex items-center gap-2' + copiedDiv.innerHTML = ' Copied!' + document.body.appendChild(copiedDiv) + + setTimeout(() => { + copiedDiv.remove() + }, 2000) + } + + return ( + +
+ {/* Header */} +
+
+ + +
+ +
+ + + +
+
+ + {/* Main Content */} +
+ {/* Code Editor */} +
+ setCode(value || '')} + options={{ + minimap: { enabled: false }, + fontSize: 14, + lineNumbers: 'on', + roundedSelection: false, + scrollBeyondLastLine: false, + automaticLayout: true, + wordWrap: 'on' + }} + /> +
+ + {/* Output Panel */} +
+
+ + + Output + + +
+ +
+ {error ? ( +
+
+ + Error: +
+
{error}
+
+ ) : output ? ( +
+
{output}
+ {plotPath && ( +
+

Plot saved to:

+ {plotPath} +
+ )} +
+ ) : ( +
+
+ +

Write code and click Run to see output

+

+ Python execution requires MCP tools from Claude Desktop +

+
+
+ )} +
+ + {/* Info Panel */} +
+
+ {selectedLanguage === 'python' ? ( +
+ + Python code is saved to disk. Use MCP tools to execute. +
+ ) : ( +
+ + HTML/JS executes directly in browser sandbox. +
+ )} +
+
+
+
+
+
+ ) +} \ No newline at end of file diff --git a/app/components/CodePlayground.tsx b/app/components/CodePlayground.tsx new file mode 100644 index 0000000000000000000000000000000000000000..90b082881610549bb9a14c03f57366a46153418c --- /dev/null +++ b/app/components/CodePlayground.tsx @@ -0,0 +1,668 @@ +'use client' + +import React, { useState, useRef, useEffect } from 'react' +import Window from './Window' +import Editor from '@monaco-editor/react' +import { + Code, + Play, + FileHtml, + FileCss, + FileJs, + FilePy, + FileCode, + Download, + Upload, + FloppyDisk, + Eye, + EyeSlash, + ArrowsOutSimple, + ArrowsInSimple, + Plus, + X, + Globe, + Lock, + Users, + Folder, + Terminal as TerminalIcon, + Lightning +} from '@phosphor-icons/react' + +interface CodePlaygroundProps { + onClose: () => void + userSession: string +} + +interface Tab { + id: string + name: string + language: string + content: string + isPublic?: boolean + type: 'html' | 'css' | 'javascript' | 'python' | 'react' | 'flutter' | 'typescript' +} + +interface ExecutionResult { + output: string + error?: string + timestamp: number +} + +export function CodePlayground({ onClose, userSession }: CodePlaygroundProps) { + const [activeTab, setActiveTab] = useState('main') + const [showPreview, setShowPreview] = useState(true) + const [showConsole, setShowConsole] = useState(true) + const [isFullscreen, setIsFullscreen] = useState(false) + const [isSaving, setIsSaving] = useState(false) + const [isExecuting, setIsExecuting] = useState(false) + const [executionResults, setExecutionResults] = useState([]) + const [publicFiles, setPublicFiles] = useState([]) + const previewRef = useRef(null) + const [draggedTab, setDraggedTab] = useState(null) + const [dragOverTab, setDragOverTab] = useState(null) + + // Enhanced tab icons with more languages + const getTabIcon = (type: string) => { + const icons: Record = { + 'html': , + 'css': , + 'javascript': , + 'typescript': , + 'python': , + 'react': , + 'flutter': + } + return icons[type] || + } + + const [tabs, setTabs] = useState([ + { + id: 'main', + name: 'main.py', + language: 'python', + type: 'python', + content: `# Welcome to WebOS Code Playground! +# You can write and execute Python code here + +def greet(name): + return f"Hello, {name}! Welcome to WebOS" + +# Example usage +result = greet("Developer") +print(result) + +# Your code here +for i in range(5): + print(f"Count: {i}") +`, + isPublic: false + } + ]) + + // Load public files from backend + useEffect(() => { + loadPublicFiles() + }, []) + + const loadPublicFiles = async () => { + try { + const response = await fetch('/api/code/public') + if (response.ok) { + const files = await response.json() + setPublicFiles(files) + } + } catch (error) { + console.error('Failed to load public files:', error) + } + } + + // Tab drag and drop handlers + const handleDragStart = (e: React.DragEvent, tabId: string) => { + setDraggedTab(tabId) + e.dataTransfer.effectAllowed = 'move' + } + + const handleDragOver = (e: React.DragEvent, tabId: string) => { + e.preventDefault() + if (draggedTab && draggedTab !== tabId) { + setDragOverTab(tabId) + } + } + + const handleDragLeave = () => { + setDragOverTab(null) + } + + const handleDrop = (e: React.DragEvent, targetTabId: string) => { + e.preventDefault() + + if (draggedTab && draggedTab !== targetTabId) { + const draggedIndex = tabs.findIndex(t => t.id === draggedTab) + const targetIndex = tabs.findIndex(t => t.id === targetTabId) + + if (draggedIndex !== -1 && targetIndex !== -1) { + const newTabs = [...tabs] + const [draggedTabObj] = newTabs.splice(draggedIndex, 1) + newTabs.splice(targetIndex, 0, draggedTabObj) + setTabs(newTabs) + } + } + + setDraggedTab(null) + setDragOverTab(null) + } + + const handleDragEnd = () => { + setDraggedTab(null) + setDragOverTab(null) + } + + // Create new tab with language selection + const createNewTab = (language: Tab['type']) => { + const templates: Record = { + 'python': { + name: 'script.py', + lang: 'python', + content: `# Python Script +def main(): + print("Hello from Python!") + +if __name__ == "__main__": + main() +` + }, + 'javascript': { + name: 'script.js', + lang: 'javascript', + content: `// JavaScript Code +console.log("Hello from JavaScript!"); + +function calculate(a, b) { + return a + b; +} + +console.log("Result:", calculate(5, 3)); +` + }, + 'typescript': { + name: 'script.ts', + lang: 'typescript', + content: `// TypeScript Code +interface User { + name: string; + age: number; +} + +function greetUser(user: User): void { + console.log(\`Hello, \${user.name}! You are \${user.age} years old.\`); +} + +greetUser({ name: "Alice", age: 25 }); +` + }, + 'react': { + name: 'App.jsx', + lang: 'javascript', + content: `// React Component +import React, { useState } from 'react'; + +function App() { + const [count, setCount] = useState(0); + + return ( +
+

React Counter

+

Count: {count}

+ +
+ ); +} + +export default App; +` + }, + 'flutter': { + name: 'main.dart', + lang: 'dart', + content: `// Flutter Widget +import 'package:flutter/material.dart'; + +class MyWidget extends StatefulWidget { + @override + _MyWidgetState createState() => _MyWidgetState(); +} + +class _MyWidgetState extends State { + int counter = 0; + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(title: Text('Flutter App')), + body: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text('Counter: $counter'), + ElevatedButton( + onPressed: () => setState(() => counter++), + child: Text('Increment'), + ), + ], + ), + ), + ); + } +} +` + }, + 'html': { + name: 'index.html', + lang: 'html', + content: ` + + + HTML Page + + +

Hello WebOS!

+ +` + }, + 'css': { + name: 'style.css', + lang: 'css', + content: `/* CSS Styles */ +body { + font-family: Arial, sans-serif; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); +} +` + } + } + + const template = templates[language] + const newTab: Tab = { + id: `tab_${Date.now()}`, + name: template.name, + language: template.lang, + type: language, + content: template.content, + isPublic: false + } + + setTabs(prev => [...prev, newTab]) + setActiveTab(newTab.id) + } + + // Execute code based on language + const executeCode = async () => { + setIsExecuting(true) + const activeTabData = tabs.find(t => t.id === activeTab) + + if (!activeTabData) { + setIsExecuting(false) + return + } + + try { + const response = await fetch('/api/code/execute', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + sessionId: userSession, + language: activeTabData.type, + code: activeTabData.content, + timestamp: Date.now() + }) + }) + + const result = await response.json() + + setExecutionResults(prev => [...prev, { + output: result.output || 'No output', + error: result.error, + timestamp: Date.now() + }]) + } catch (error) { + setExecutionResults(prev => [...prev, { + output: '', + error: `Execution failed: ${error}`, + timestamp: Date.now() + }]) + } + + setIsExecuting(false) + } + + // Save to public folder + const saveToPublic = async () => { + const activeTabData = tabs.find(t => t.id === activeTab) + if (!activeTabData) return + + setIsSaving(true) + try { + const response = await fetch('/api/code/public/save', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + sessionId: userSession, + file: { + ...activeTabData, + isPublic: true, + author: userSession + } + }) + }) + + if (response.ok) { + setTabs(prev => prev.map(tab => + tab.id === activeTab ? { ...tab, isPublic: true } : tab + )) + await loadPublicFiles() + } + } catch (error) { + console.error('Failed to save to public:', error) + } + setIsSaving(false) + } + + const handleEditorChange = (value: string | undefined) => { + if (!value) return + + setTabs(prev => prev.map(tab => + tab.id === activeTab ? { ...tab, content: value } : tab + )) + } + + const activeTabContent = tabs.find(t => t.id === activeTab) + + // Close tab + const closeTab = (tabId: string) => { + if (tabs.length === 1) return // Keep at least one tab + + setTabs(prev => prev.filter(t => t.id !== tabId)) + if (activeTab === tabId) { + setActiveTab(tabs[0].id) + } + } + + // Load file from public + const loadPublicFile = (file: Tab) => { + const newTab: Tab = { + ...file, + id: `tab_${Date.now()}`, + isPublic: false + } + setTabs(prev => [...prev, newTab]) + setActiveTab(newTab.id) + } + + return ( + +
+ {/* Toolbar */} +
+
+ + Multi-Language Playground +
+ + {userSession.substring(0, 8)} +
+
+ +
+ + + + + + + + + +
+
+ + {/* Main Content Area */} +
+ {/* Sidebar - Public Files */} +
+
+
+ + Public Files +
+
+
+ {publicFiles.map(file => ( + + ))} +
+
+ + {/* Editor Section */} +
+ {/* Tabs with drag and drop */} +
+
+ {tabs.map(tab => ( +
handleDragStart(e, tab.id)} + onDragOver={(e) => handleDragOver(e, tab.id)} + onDragLeave={handleDragLeave} + onDrop={(e) => handleDrop(e, tab.id)} + onDragEnd={handleDragEnd} + className={` + flex items-center gap-2 px-3 py-2 text-sm border-r border-[#3e3e3e] + cursor-move transition-all select-none + ${activeTab === tab.id ? 'bg-[#1e1e1e] text-white' : 'text-gray-400 hover:text-white hover:bg-[#2d2d2d]'} + ${dragOverTab === tab.id ? 'border-l-2 border-l-blue-500' : ''} + `} + onClick={() => setActiveTab(tab.id)} + > + {getTabIcon(tab.type)} + {tab.name} + {tab.isPublic && ( + + )} + {tabs.length > 1 && ( + + )} +
+ ))} +
+ + {/* Add new tab dropdown */} +
+ +
+ + + + + + + +
+
+
+ + {/* Monaco Editor */} +
+ +
+ + {/* Console Output */} + {showConsole && ( +
+
+ Console Output + +
+
+ {executionResults.map((result, index) => ( +
+
+ [{new Date(result.timestamp).toLocaleTimeString()}] +
+ {result.error ? ( +
{result.error}
+ ) : ( +
{result.output}
+ )} +
+ ))} +
+
+ )} +
+ + {/* Preview Section (for HTML/CSS/JS) */} + {showPreview && (activeTabContent?.type === 'html' || activeTabContent?.type === 'css' || activeTabContent?.type === 'javascript') && ( +
+
+ Live Preview + +
+