Reubencf commited on
Commit
8af739b
·
1 Parent(s): bc0dab9

First Push

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .claude/settings.local.json +11 -0
  2. APPLICATIONS_GUIDE.md +200 -0
  3. CLAUDE_DESKTOP_SETUP.md +99 -0
  4. DEPLOYMENT.md +107 -0
  5. Dockerfile +69 -0
  6. Empc-hackathonbackendmcp_server.py +0 -0
  7. Empc-hackathonpublicbackground_readme.txt +1 -0
  8. HACKATHON_README.md +215 -0
  9. MCP_SETUP_GUIDE.md +201 -0
  10. TEST_MCP_CONNECTION.md +76 -0
  11. TTT_CLAUDE_DESKTOP_GUIDE.md +257 -0
  12. USE_NGROK.md +27 -0
  13. app/api/code/execute/route.ts +186 -0
  14. app/api/code/public/route.ts +85 -0
  15. app/api/code/save/route.ts +130 -0
  16. app/api/download/route.ts +99 -0
  17. app/api/files/route.ts +183 -0
  18. app/api/gemini/chat/route.ts +113 -0
  19. app/api/gemini/transcribe/route.ts +94 -0
  20. app/api/public/route.ts +178 -0
  21. app/api/upload/route.ts +84 -0
  22. app/components/AboutModal.tsx +179 -0
  23. app/components/BackgroundSelector.tsx +217 -0
  24. app/components/Calendar.tsx +301 -0
  25. app/components/ClaudeIntegration.tsx +240 -0
  26. app/components/Clock.tsx +188 -0
  27. app/components/CodeExecutor.tsx +306 -0
  28. app/components/CodePlayground.tsx +668 -0
  29. app/components/ContextMenu.tsx +90 -0
  30. app/components/Desktop.tsx +498 -0
  31. app/components/DesktopContextMenu.tsx +123 -0
  32. app/components/DesktopIcon.tsx +64 -0
  33. app/components/Dock.tsx +170 -0
  34. app/components/DraggableDesktopIcon.tsx +146 -0
  35. app/components/FileManager.tsx +542 -0
  36. app/components/FilePreview.tsx +372 -0
  37. app/components/GeminiChat.tsx +258 -0
  38. app/components/HelpModal.tsx +172 -0
  39. app/components/MatrixRain.tsx +236 -0
  40. app/components/PDFViewer.tsx +143 -0
  41. app/components/SpotlightSearch.tsx +182 -0
  42. app/components/Terminal.tsx +223 -0
  43. app/components/TopBar.tsx +122 -0
  44. app/components/VSCodeEditor.tsx +458 -0
  45. app/components/WebBrowser.tsx +277 -0
  46. app/components/WebBrowserApp.tsx +438 -0
  47. app/components/Window.tsx +132 -0
  48. app/gemini/page.tsx +426 -0
  49. app/globals.css +301 -12
  50. app/hooks/useKV.ts +37 -0
.claude/settings.local.json ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "permissions": {
3
+ "allow": [
4
+ "Bash(dir:*)",
5
+ "Bash(del nul)",
6
+ "Bash(rm:*)"
7
+ ],
8
+ "deny": [],
9
+ "ask": []
10
+ }
11
+ }
APPLICATIONS_GUIDE.md ADDED
@@ -0,0 +1,200 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Applications Guide
2
+
3
+ ## Integrated Desktop Applications
4
+
5
+ Your web-based desktop OS now includes the following applications:
6
+
7
+ ### 1. **Web Browser** 🌐
8
+ - **Icon**: Globe icon (blue/cyan gradient)
9
+ - **Location**: Desktop + Dock
10
+ - **Features**:
11
+ - Multi-tab browsing
12
+ - Navigation controls (back, forward, refresh, home)
13
+ - URL bar with HTTPS indicator
14
+ - Quick links to iframe-friendly sites
15
+ - CORS proxy option for better website compatibility
16
+ - Fullscreen mode
17
+ - Open in new window option
18
+ - Resizable and draggable window
19
+
20
+ **CORS Proxy Solution**:
21
+ - Toggle in settings to enable/disable
22
+ - Uses `allorigins.win` proxy by default
23
+ - Bypasses cross-origin restrictions
24
+ - May affect performance slightly
25
+
26
+ **Quick Access Sites**:
27
+ - Google
28
+ - Wikipedia
29
+ - MDN Web Docs
30
+ - Stack Overflow
31
+ - GitHub
32
+ - DuckDuckGo
33
+
34
+ ### 2. **Gemini Chat** ✨
35
+ - **Icon**: Sparkle icon (orange gradient #E95420)
36
+ - **Location**: Desktop + Dock
37
+ - **Features**:
38
+ - AI-powered chat interface
39
+ - Conversation history (saved to localStorage)
40
+ - Message timestamps
41
+ - Draggable and resizable window
42
+ - Clean, modern UI matching Ubuntu style
43
+ - API key management (stored locally)
44
+
45
+ **Setup**:
46
+ 1. Click the Gemini Chat icon on desktop or dock
47
+ 2. Enter your Gemini API key (get from [Google AI Studio](https://makersuite.google.com/app/apikey))
48
+ 3. Start chatting!
49
+
50
+ **Features**:
51
+ - Context-aware responses (last 10 messages)
52
+ - Real-time typing indicators
53
+ - Clear chat history option
54
+ - Persistent chat across sessions
55
+ - Secure API key storage
56
+
57
+ ### 3. **File Manager** 📁
58
+ - Document management
59
+ - File browsing
60
+ - Multiple file type support
61
+
62
+ ### 4. **Calendar** 📅
63
+ - Exam scheduling
64
+ - Date management
65
+ - Event tracking
66
+
67
+ ### 5. **Help** ❓
68
+ - System help and information
69
+ - Usage guides
70
+
71
+ ## API Integration
72
+
73
+ ### Gemini API Routes
74
+
75
+ **Chat Endpoint**: `/api/gemini/chat`
76
+ - Method: POST
77
+ - Body: `{ message, apiKey, history }`
78
+ - Response: `{ response }` or `{ error }`
79
+ - Uses: Gemini 1.5 Flash model
80
+
81
+ **Transcription Endpoint**: `/api/gemini/transcribe`
82
+ - Method: POST
83
+ - Body: FormData with audio file and apiKey
84
+ - Response: `{ transcription }` or `{ error }`
85
+ - Note: Full audio support requires Gemini 1.5 Pro
86
+
87
+ ## Accessing Applications
88
+
89
+ ### Desktop Icons
90
+ Click any icon on the desktop (center of screen):
91
+ - Documents
92
+ - Exam Calendar
93
+ - Web Browser
94
+ - Gemini Chat
95
+
96
+ ### Dock (Left sidebar)
97
+ Quick access to all applications:
98
+ - Applications menu
99
+ - Documents
100
+ - Exam Calendar
101
+ - Web Browser
102
+ - Gemini Chat
103
+ - Help
104
+
105
+ ## Window Management
106
+
107
+ All applications support:
108
+ - **Dragging**: Click and drag the title bar
109
+ - **Resizing**: Drag from edges or corners
110
+ - **Closing**: Click the red close button (top-left)
111
+ - **Minimize/Maximize**: Click window control buttons
112
+
113
+ ## Browser Usage Tips
114
+
115
+ 1. **For Best Compatibility**: Enable CORS proxy in browser settings
116
+ 2. **Interactive Sites**: Some features may not work through proxy
117
+ 3. **Performance**: Direct access (no proxy) is faster but limited
118
+ 4. **Quick Links**: Use provided shortcuts for reliable access
119
+
120
+ ## Gemini Chat Tips
121
+
122
+ 1. **API Key**: Keep it secure, it's stored locally in your browser
123
+ 2. **Context**: The AI remembers last 10 messages in conversation
124
+ 3. **Clear History**: Use "Clear" button to start fresh conversation
125
+ 4. **Error Handling**: Check API key if you encounter errors
126
+ 5. **Conversation**: Chat naturally, Gemini understands context
127
+
128
+ ## Technical Details
129
+
130
+ ### Technologies Used
131
+ - **Frontend**: React 19, Next.js 16, TypeScript
132
+ - **Styling**: Tailwind CSS 4
133
+ - **Icons**: Phosphor Icons
134
+ - **Animations**: Framer Motion
135
+ - **State**: React hooks + localStorage
136
+
137
+ ### File Structure
138
+ ```
139
+ app/
140
+ ├── components/
141
+ │ ├── Desktop.tsx # Main desktop container
142
+ │ ├── Dock.tsx # Application dock
143
+ │ ├── DesktopIcon.tsx # Desktop icon component
144
+ │ ├── WebBrowserApp.tsx # Browser application
145
+ │ ├── GeminiChat.tsx # Gemini chat application
146
+ │ └── ...
147
+ ├── api/
148
+ │ └── gemini/
149
+ │ ├── chat/route.ts # Chat API endpoint
150
+ │ └── transcribe/route.ts # Transcription endpoint
151
+ └── page.tsx # Main entry point
152
+ ```
153
+
154
+ ## Future Enhancements
155
+
156
+ Potential improvements:
157
+ - [ ] Browser bookmarks system
158
+ - [ ] Download manager
159
+ - [ ] Browser history
160
+ - [ ] Gemini voice input
161
+ - [ ] Gemini image analysis
162
+ - [ ] Multiple chat sessions
163
+ - [ ] Chat export/import
164
+ - [ ] Custom themes
165
+ - [ ] Keyboard shortcuts
166
+ - [ ] Window snapping
167
+
168
+ ## Troubleshooting
169
+
170
+ ### Browser Not Loading Sites
171
+ - Enable CORS proxy in settings
172
+ - Try quick link sites first
173
+ - Some sites block iframe embedding
174
+
175
+ ### Gemini Chat Not Working
176
+ - Verify API key is correct
177
+ - Check internet connection
178
+ - Ensure API key has proper permissions
179
+ - Try clearing chat and starting fresh
180
+
181
+ ### Window Issues
182
+ - Refresh page if window becomes unresponsive
183
+ - Check that window isn't dragged off-screen
184
+ - Clear localStorage if persistent issues
185
+
186
+ ## Development
187
+
188
+ To run the application:
189
+ ```bash
190
+ npm run dev
191
+ ```
192
+
193
+ Access at: `http://localhost:3000`
194
+
195
+ ## API Keys
196
+
197
+ You'll need:
198
+ - **Gemini API Key**: From [Google AI Studio](https://makersuite.google.com/app/apikey)
199
+
200
+ Keys are stored locally in browser's localStorage for convenience.
CLAUDE_DESKTOP_SETUP.md ADDED
@@ -0,0 +1,99 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Setting Up MCP with Claude Desktop
2
+
3
+ ## Step 1: Find Your Claude Desktop Config File
4
+
5
+ Open File Explorer and navigate to:
6
+ ```
7
+ C:\Users\[YourUsername]\AppData\Roaming\Claude\
8
+ ```
9
+
10
+ Or press `Win+R` and type:
11
+ ```
12
+ %APPDATA%\Claude
13
+ ```
14
+
15
+ ## Step 2: Edit claude_desktop_config.json
16
+
17
+ If the file doesn't exist, create it. Add this content:
18
+
19
+ ```json
20
+ {
21
+ "mcpServers": {
22
+ "semsoon": {
23
+ "command": "python",
24
+ "args": ["E:\\mpc-hackathon\\backend\\mcp_server.py"],
25
+ "env": {
26
+ "UPLOAD_PASSCODE": "semsoon-secure-2025",
27
+ "DATA_DIR": "E:\\mpc-hackathon\\data"
28
+ }
29
+ }
30
+ }
31
+ }
32
+ ```
33
+
34
+ ## Step 3: Restart Claude Desktop
35
+
36
+ 1. Completely close Claude Desktop (check system tray)
37
+ 2. Open Claude Desktop again
38
+ 3. The MCP server should start automatically
39
+
40
+ ## Step 4: Test in Claude Desktop
41
+
42
+ Open a new conversation and try these:
43
+
44
+ 1. **Basic test**: "What MCP servers are available?"
45
+ - Claude should mention "semsoon"
46
+
47
+ 2. **List documents**: "Using semsoon, show me all available documents"
48
+ - Should list the sample documents
49
+
50
+ 3. **Check exams**: "Using semsoon, what exams are scheduled?"
51
+ - Should show the sample exams
52
+
53
+ 4. **Read a file**: "Using semsoon, read the study_tips.md file"
54
+ - Should display the content
55
+
56
+ ## If It's Not Working
57
+
58
+ ### Check 1: Python Path
59
+ Make sure Python is in your PATH:
60
+ ```cmd
61
+ python --version
62
+ ```
63
+
64
+ If not, use full path in config:
65
+ ```json
66
+ "command": "C:\\Python311\\python.exe"
67
+ ```
68
+
69
+ ### Check 2: Dependencies
70
+ ```cmd
71
+ pip install fastmcp PyPDF2 python-docx python-dotenv
72
+ ```
73
+
74
+ ### Check 3: Test Server Manually
75
+ ```cmd
76
+ cd E:\mpc-hackathon\backend
77
+ python mcp_server.py
78
+ ```
79
+ Should show: "Uvicorn running on http://127.0.0.1:8000"
80
+
81
+ ### Check 4: Claude Desktop Logs
82
+ In Claude Desktop, press `Ctrl+Shift+I` to open DevTools and check Console for errors.
83
+
84
+ ## What Success Looks Like
85
+
86
+ When properly connected, Claude Desktop will:
87
+ 1. Show "semsoon" as an available MCP server
88
+ 2. Be able to list documents
89
+ 3. Be able to show exam schedules
90
+ 4. Be able to read files
91
+ 5. Be able to upload files (with passcode)
92
+
93
+ ## Current Server Status
94
+
95
+ ✅ Your MCP server is currently running at http://localhost:8000
96
+ ✅ Sample documents have been created in E:\mpc-hackathon\data\documents
97
+ ✅ Sample exams have been added to the database
98
+
99
+ Just add the config to Claude Desktop and restart it!
DEPLOYMENT.md ADDED
@@ -0,0 +1,107 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Deployment Guide for Hugging Face Spaces
2
+
3
+ ## Prerequisites
4
+ - Hugging Face account
5
+ - Hugging Face Spaces access
6
+ - Git installed locally
7
+
8
+ ## Deployment Steps
9
+
10
+ ### 1. Create a New Space on Hugging Face
11
+ 1. Go to https://huggingface.co/spaces
12
+ 2. Click "Create new Space"
13
+ 3. Choose a name for your space
14
+ 4. Select "Docker" as the SDK
15
+ 5. Choose visibility (public/private)
16
+ 6. Click "Create Space"
17
+
18
+ ### 2. Configure Environment Variables
19
+ In your Hugging Face Space settings:
20
+ 1. Go to Settings → Variables and Secrets
21
+ 2. Add the following secrets:
22
+ - `GEMINI_API_KEY`: Your Google Gemini API key
23
+ - `UPLOAD_PASSCODE`: Your secure upload passcode
24
+ - `MCP_SERVER_URL`: The MCP backend URL (if external)
25
+
26
+ ### 3. Prepare Your Repository
27
+ ```bash
28
+ # Clone your HF Space repository
29
+ git clone https://huggingface.co/spaces/YOUR_USERNAME/YOUR_SPACE_NAME
30
+ cd YOUR_SPACE_NAME
31
+
32
+ # Copy all project files
33
+ cp -r /path/to/mpc-hackathon/* .
34
+
35
+ # Make sure Dockerfile is in root
36
+ ls Dockerfile
37
+
38
+ # Add all files
39
+ git add .
40
+ git commit -m "Initial deployment"
41
+ ```
42
+
43
+ ### 4. Deploy to Hugging Face Spaces
44
+ ```bash
45
+ # Push to Hugging Face
46
+ git push origin main
47
+ ```
48
+
49
+ ### 5. Monitor Deployment
50
+ - Check the build logs in your Space
51
+ - The app will be available at: https://YOUR_USERNAME-YOUR_SPACE_NAME.hf.space
52
+
53
+ ## Docker Configuration
54
+
55
+ The Dockerfile is configured to:
56
+ 1. Build the Next.js application
57
+ 2. Install Python backend dependencies
58
+ 3. Run both frontend (port 3000) and backend (port 8000)
59
+ 4. Create necessary data directories
60
+ 5. Run as non-root user for security
61
+
62
+ ## File Structure for Deployment
63
+ ```
64
+ /
65
+ ├── app/ # Next.js application
66
+ ├── backend/ # Python MCP backend
67
+ ├── public/ # Static assets
68
+ ├── data/ # Data storage (created at runtime)
69
+ ├── Dockerfile # Docker configuration
70
+ ├── package.json # Node dependencies
71
+ ├── requirements.txt # Python dependencies
72
+ ├── next.config.mjs # Next.js configuration
73
+ └── .env # Environment variables (create from .env.example)
74
+ ```
75
+
76
+ ## Troubleshooting
77
+
78
+ ### Build Failures
79
+ - Check Dockerfile syntax
80
+ - Verify all dependencies in package.json and requirements.txt
81
+ - Check HF Space logs for specific errors
82
+
83
+ ### Runtime Issues
84
+ - Verify environment variables are set
85
+ - Check port configurations
86
+ - Ensure data directories have proper permissions
87
+
88
+ ### Performance
89
+ - HF Spaces have resource limits
90
+ - Consider optimizing images and assets
91
+ - Use production builds
92
+
93
+ ## Security Notes
94
+ - Never commit `.env` files with real credentials
95
+ - Use HF Spaces secrets for sensitive data
96
+ - The app runs as non-root user for security
97
+ - Upload passcode protects write operations
98
+
99
+ ## Updates
100
+ To update your deployed app:
101
+ ```bash
102
+ git add .
103
+ git commit -m "Update description"
104
+ git push origin main
105
+ ```
106
+
107
+ The Space will automatically rebuild and redeploy.
Dockerfile ADDED
@@ -0,0 +1,69 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Use Node.js 20 as the base image
2
+ FROM node:20-alpine AS base
3
+
4
+ # Install dependencies only when needed
5
+ FROM base AS deps
6
+ RUN apk add --no-cache libc6-compat
7
+ WORKDIR /app
8
+
9
+ # Install dependencies
10
+ COPY package.json package-lock.json* ./
11
+ RUN npm ci
12
+
13
+ # Build the application
14
+ FROM base AS builder
15
+ WORKDIR /app
16
+ COPY --from=deps /app/node_modules ./node_modules
17
+ COPY . .
18
+
19
+ # Set environment variables for build
20
+ ENV NEXT_TELEMETRY_DISABLED 1
21
+
22
+ # Build Next.js application
23
+ RUN npm run build
24
+
25
+ # Production image
26
+ FROM base AS runner
27
+ WORKDIR /app
28
+
29
+ ENV NODE_ENV production
30
+ ENV NEXT_TELEMETRY_DISABLED 1
31
+
32
+ # Create a non-root user
33
+ RUN addgroup --system --gid 1001 nodejs
34
+ RUN adduser --system --uid 1001 nextjs
35
+
36
+ # Copy built application
37
+ COPY --from=builder /app/public ./public
38
+ COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
39
+ COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
40
+
41
+ # Create data directory for file uploads
42
+ RUN mkdir -p /app/data/documents /app/data/public /app/data/exams
43
+ RUN chown -R nextjs:nodejs /app/data
44
+
45
+ # Install Python for MCP backend
46
+ RUN apk add --no-cache python3 py3-pip
47
+
48
+ # Copy Python backend
49
+ COPY --chown=nextjs:nodejs backend /app/backend
50
+ WORKDIR /app/backend
51
+
52
+ # Install Python dependencies
53
+ COPY requirements.txt .
54
+ RUN pip3 install --no-cache-dir -r requirements.txt
55
+
56
+ WORKDIR /app
57
+
58
+ # Switch to non-root user
59
+ USER nextjs
60
+
61
+ # Expose ports
62
+ EXPOSE 3000 8000
63
+
64
+ # Health check
65
+ HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
66
+ CMD node -e "require('http').get('http://localhost:3000/api/health', (res) => process.exit(res.statusCode === 200 ? 0 : 1))"
67
+
68
+ # Start both services
69
+ CMD ["sh", "-c", "python3 backend/mcp_server.py & node server.js"]
Empc-hackathonbackendmcp_server.py ADDED
File without changes
Empc-hackathonpublicbackground_readme.txt ADDED
@@ -0,0 +1 @@
 
 
1
+ Please add your background.webp file to the public directory at E:\mpc-hackathon\public\background.webp
HACKATHON_README.md ADDED
@@ -0,0 +1,215 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # 🎓 Exam Hub - AI-Powered Academic Assistant
2
+
3
+ ## 🏆 MCP Hackathon Submission
4
+
5
+ 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.
6
+
7
+ ## 🌟 What Makes This Special
8
+
9
+ ### 🚀 First-Mover Innovation
10
+ - **Early MCP Adopter**: One of the first applications built with Anthropic's brand-new Model Context Protocol
11
+ - **AI-Native Design**: Not just AI-enabled, but designed from the ground up for AI interaction
12
+ - **Practical Application**: Solves real problems students face every day
13
+
14
+ ### 💡 Key Features
15
+
16
+ #### 📚 Smart Document Management
17
+ - Upload and organize study materials
18
+ - AI-powered document search and extraction
19
+ - PDF and DOCX text extraction
20
+ - Full-text search within documents
21
+ - Automatic categorization by subject
22
+
23
+ #### 📅 Intelligent Exam Calendar
24
+ - Track all upcoming exams in one place
25
+ - Get AI-generated study schedules
26
+ - Find related documents for each exam
27
+ - Urgency-based exam prioritization
28
+ - Study session tracking
29
+
30
+ #### 🤖 Claude Integration (MCP)
31
+ - Natural language queries: "What exams do I have this week?"
32
+ - Smart document retrieval: "Find all chemistry notes"
33
+ - Study assistance: "Create a study plan for my math midterm"
34
+ - Document summarization: "Summarize the key points from chapter 5"
35
+
36
+ #### 🎨 Beautiful Ubuntu-Style Desktop UI
37
+ - Familiar desktop metaphor
38
+ - File manager with folder navigation
39
+ - Calendar application
40
+ - Matrix rain effect (because why not!)
41
+ - Responsive and intuitive design
42
+
43
+ ## 🛠️ Technical Stack
44
+
45
+ ### Frontend
46
+ - **Next.js 14** - React framework with App Router
47
+ - **Tailwind CSS** - Utility-first styling
48
+ - **Framer Motion** - Smooth animations
49
+ - **Phosphor Icons** - Beautiful icon set
50
+
51
+ ### Backend
52
+ - **FastMCP** - Model Context Protocol server
53
+ - **Python** - Backend logic
54
+ - **SQLite** - Exam database
55
+ - **FastAPI** - REST API for demos
56
+
57
+ ### AI Integration
58
+ - **MCP Tools** - 20+ custom tools for Claude
59
+ - **Document Intelligence** - PDF/DOCX extraction
60
+ - **Natural Language Interface** - Talk to your documents
61
+
62
+ ## 🚦 Quick Start
63
+
64
+ ### 1. Install Dependencies
65
+
66
+ ```bash
67
+ # Frontend
68
+ npm install
69
+
70
+ # Backend
71
+ pip install -r requirements.txt
72
+ ```
73
+
74
+ ### 2. Run the Application
75
+
76
+ ```bash
77
+ # Start Next.js UI (Terminal 1)
78
+ npm run dev
79
+
80
+ # Start MCP Server (Terminal 2)
81
+ cd backend
82
+ python mcp_server.py
83
+
84
+ # Optional: Start Demo API (Terminal 3)
85
+ cd backend
86
+ python demo_api.py
87
+ ```
88
+
89
+ ### 3. Access the Application
90
+
91
+ - **Web UI**: http://localhost:3000
92
+ - **MCP Server**: http://localhost:8000
93
+ - **Demo API**: http://localhost:8001/docs
94
+
95
+ ## 🎮 Demo Instructions
96
+
97
+ ### Web UI Demo
98
+ 1. Open http://localhost:3000
99
+ 2. Click on the File Manager icon
100
+ 3. Browse through the virtual file system
101
+ 4. Open the Calendar to see upcoming exams
102
+
103
+ ### Claude Desktop Integration
104
+ 1. Install Claude Desktop
105
+ 2. The configuration has been added to: `%APPDATA%\Claude\claude_desktop_config.json`
106
+ 3. Restart Claude Desktop
107
+ 4. Try: "Show me all documents in the exam hub"
108
+
109
+ ### API Demo
110
+ 1. Open http://localhost:8001/docs
111
+ 2. Try the interactive API documentation
112
+ 3. Test endpoints like `/exams/upcoming` or `/documents`
113
+
114
+ ## 📊 MCP Tools Available
115
+
116
+ ### Document Tools (11 tools)
117
+ - `list_all_documents` - View all files
118
+ - `get_folder_structure` - Browse folders
119
+ - `search_documents` - Find files by name
120
+ - `extract_pdf_text` - Read PDFs
121
+ - `extract_docx_text` - Read Word docs
122
+ - `read_text_file` - Read text files
123
+ - `search_in_document` - Search within files
124
+ - `get_document_summary` - Get file previews
125
+ - `upload_file` - Upload new files (secured)
126
+ - `create_folder` - Create directories
127
+ - `delete_file` - Remove files
128
+
129
+ ### Calendar Tools (10 tools)
130
+ - `get_all_exams` - List all exams
131
+ - `get_exams_this_week` - Current week
132
+ - `get_exams_next_week` - Next week
133
+ - `get_exams_by_month` - Monthly view
134
+ - `find_exam_by_subject` - Subject search
135
+ - `get_exams_by_date` - Specific date
136
+ - `get_upcoming_exams` - Next N days
137
+ - `search_related_documents` - Find study materials
138
+ - `get_study_schedule` - Study sessions
139
+ - `get_exam_statistics` - Analytics
140
+
141
+ ## 🎯 Use Cases
142
+
143
+ ### For Students
144
+ - "What exams are coming up this week?"
145
+ - "Find all my chemistry notes"
146
+ - "Create a study schedule for finals"
147
+ - "Summarize this PDF for me"
148
+
149
+ ### For Educators
150
+ - Track student exam schedules
151
+ - Organize course materials
152
+ - Generate study guides
153
+ - Monitor academic calendars
154
+
155
+ ## 🔒 Security
156
+
157
+ - **Passcode Protection**: Upload operations require authentication
158
+ - **Local Storage**: All data stays on your machine
159
+ - **Secure Design**: No external data transmission
160
+
161
+ ## 🌍 Deployment Options
162
+
163
+ ### Hugging Face Spaces
164
+ - Ready for deployment with Docker
165
+ - Persistent storage configured
166
+ - Environment variables set
167
+
168
+ ### Local Installation
169
+ - Works on Windows, Mac, Linux
170
+ - No internet required
171
+ - Full privacy control
172
+
173
+ ## 📈 Future Enhancements
174
+
175
+ - [ ] Mobile app version
176
+ - [ ] Collaborative study groups
177
+ - [ ] AI-generated practice exams
178
+ - [ ] Integration with university systems
179
+ - [ ] Voice commands
180
+ - [ ] Smart notifications
181
+
182
+ ## 🏗️ Architecture
183
+
184
+ ```
185
+ ┌─────────────────┐ ┌─────────────────┐
186
+ │ Next.js UI │────▶│ REST API │
187
+ └─────────────────┘ └─────────────────┘
188
+ │ │
189
+ │ ▼
190
+ │ ┌─────────────────┐
191
+ └─────────────▶│ MCP Server │
192
+ └─────────────────┘
193
+
194
+ ┌────────┴────────┐
195
+ ▼ ▼
196
+ ┌──────────┐ ┌──────────┐
197
+ │ Documents│ │ SQLite │
198
+ └──────────┘ └──────────┘
199
+ ```
200
+
201
+ ## 👏 Acknowledgments
202
+
203
+ - Built for the Anthropic MCP Hackathon
204
+ - Powered by Claude and the Model Context Protocol
205
+ - Ubuntu desktop design inspiration
206
+
207
+ ## 📝 License
208
+
209
+ MIT License - Feel free to use and modify!
210
+
211
+ ---
212
+
213
+ **Built with ❤️ for students everywhere**
214
+
215
+ *Making exam preparation smarter with AI*
MCP_SETUP_GUIDE.md ADDED
@@ -0,0 +1,201 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # MCP Integration Setup Guide for Claude Desktop
2
+
3
+ ## Quick Start
4
+
5
+ ### 1. Locate Claude Desktop Config File
6
+
7
+ The Claude Desktop configuration file is located at:
8
+ - **Windows**: `%APPDATA%\Claude\claude_desktop_config.json`
9
+ - **macOS**: `~/Library/Application Support/Claude/claude_desktop_config.json`
10
+ - **Linux**: `~/.config/Claude/claude_desktop_config.json`
11
+
12
+ ### 2. Add MCP Server Configuration
13
+
14
+ Open the config file and add this configuration:
15
+
16
+ ```json
17
+ {
18
+ "mcpServers": {
19
+ "exam-hub": {
20
+ "command": "python",
21
+ "args": ["E:\\mpc-hackathon\\backend\\mcp_server.py"],
22
+ "env": {
23
+ "UPLOAD_PASSCODE": "exam-hub-secure-2025",
24
+ "DATA_DIR": "E:\\mpc-hackathon\\data"
25
+ }
26
+ }
27
+ }
28
+ }
29
+ ```
30
+
31
+ **Note**: Adjust the path to match your actual project location.
32
+
33
+ ### 3. Install Python Dependencies
34
+
35
+ Make sure you have Python installed and run:
36
+
37
+ ```bash
38
+ cd E:\mpc-hackathon
39
+ pip install -r requirements.txt
40
+ ```
41
+
42
+ ### 4. Restart Claude Desktop
43
+
44
+ After updating the configuration:
45
+ 1. Completely close Claude Desktop
46
+ 2. Reopen Claude Desktop
47
+ 3. The MCP server should start automatically
48
+
49
+ ## Testing the MCP Integration
50
+
51
+ ### Test Commands to Try in Claude Desktop
52
+
53
+ Once connected, try these commands in a new Claude Desktop conversation:
54
+
55
+ #### 1. Check Server Status
56
+ Ask: "Can you check the exam hub server status?"
57
+ - Claude should use the `get_server_info` tool
58
+
59
+ #### 2. List Documents
60
+ Ask: "What documents are available?"
61
+ - Claude should use the `list_all_documents` tool
62
+
63
+ #### 3. View Exam Schedule
64
+ Ask: "What exams are coming up this week?"
65
+ - Claude should use the `get_exams_this_week` tool
66
+
67
+ #### 4. Search for Documents
68
+ Ask: "Find all PDF files"
69
+ - Claude should use the `search_documents` tool with file_type="pdf"
70
+
71
+ #### 5. Get Exam Statistics
72
+ Ask: "Show me exam statistics"
73
+ - Claude should use the `get_exam_statistics` tool
74
+
75
+ #### 6. Read a Document
76
+ Ask: "Read the study_tips.md file"
77
+ - Claude should use the `read_text_file` tool
78
+
79
+ #### 7. Search Within Documents
80
+ Ask: "Search for 'exam' in all documents"
81
+ - Claude should use the `search_in_document` tool
82
+
83
+ #### 8. Upload a File (Requires Passcode)
84
+ Ask: "Upload a test file with content 'Hello World' as test.txt"
85
+ - Claude will ask for the passcode
86
+ - Provide: `exam-hub-secure-2025`
87
+ - Claude should use the `upload_file` tool
88
+
89
+ ## Available MCP Tools
90
+
91
+ ### Document Management Tools (No Passcode)
92
+ - `list_all_documents()` - List all files with metadata
93
+ - `get_folder_structure()` - View folder hierarchy
94
+ - `search_documents(query, file_type)` - Search files by name
95
+ - `extract_pdf_text(filename, max_pages)` - Extract PDF content
96
+ - `extract_docx_text(filename)` - Extract Word document content
97
+ - `read_text_file(filename)` - Read text files
98
+ - `search_in_document(filename, query)` - Search within files
99
+ - `get_document_summary(filename)` - Get file preview and metadata
100
+
101
+ ### Document Management Tools (Passcode Required)
102
+ - `upload_file(filename, file_data, passcode)` - Upload new files
103
+ - `create_folder(folder_path, passcode)` - Create folders
104
+ - `delete_file(filename, passcode)` - Delete files
105
+
106
+ ### Exam Calendar Tools
107
+ - `get_all_exams()` - List all exams
108
+ - `get_exams_this_week()` - Current week's exams
109
+ - `get_exams_next_week()` - Next week's exams
110
+ - `get_exams_by_month(year, month)` - Exams by month
111
+ - `find_exam_by_subject(subject)` - Search by subject
112
+ - `get_exams_by_date(date)` - Exams on specific date
113
+ - `get_upcoming_exams(days)` - Upcoming exams in N days
114
+ - `search_related_documents(subject)` - Find related study materials
115
+ - `get_study_schedule(exam_id)` - View study sessions
116
+ - `get_exam_statistics()` - Exam statistics and analytics
117
+
118
+ ## Troubleshooting
119
+
120
+ ### MCP Server Not Connecting
121
+
122
+ 1. **Check Python Installation**:
123
+ ```bash
124
+ python --version
125
+ ```
126
+ Should show Python 3.8 or higher
127
+
128
+ 2. **Check Dependencies**:
129
+ ```bash
130
+ pip list | grep fastmcp
131
+ ```
132
+ Should show fastmcp installed
133
+
134
+ 3. **Test Server Manually**:
135
+ ```bash
136
+ cd E:\mpc-hackathon\backend
137
+ python mcp_server.py
138
+ ```
139
+ Should show "Uvicorn running on http://127.0.0.1:8000"
140
+
141
+ 4. **Check Claude Desktop Logs**:
142
+ - Open Developer Tools in Claude Desktop (Ctrl+Shift+I or Cmd+Option+I)
143
+ - Look for MCP-related errors in the console
144
+
145
+ ### Common Issues
146
+
147
+ **Issue**: "MCP server failed to start"
148
+ **Solution**: Check the Python path in the config file is correct
149
+
150
+ **Issue**: "No tools available"
151
+ **Solution**: Restart Claude Desktop after updating config
152
+
153
+ **Issue**: "Invalid passcode" when uploading
154
+ **Solution**: Check the UPLOAD_PASSCODE in your .env file matches what you're using
155
+
156
+ ## Environment Variables
157
+
158
+ The MCP server uses these environment variables (set in `.env` file):
159
+
160
+ ```env
161
+ UPLOAD_PASSCODE=exam-hub-secure-2025 # Required for upload operations
162
+ MCP_PORT=8000 # Server port (default: 8000)
163
+ DATA_DIR=./data # Data directory path
164
+ ```
165
+
166
+ ## Sample Data
167
+
168
+ The server automatically creates sample files on first run:
169
+ - `README.txt` - Welcome message
170
+ - `study_tips.md` - Study tips document
171
+ - `exam_schedule.txt` - Sample exam schedule
172
+
173
+ Sample exams are also added to the database:
174
+ - Mathematics Midterm (Feb 15)
175
+ - Physics Final (Feb 20)
176
+ - Computer Science Quiz (Feb 10)
177
+ - Chemistry Midterm (Feb 18)
178
+ - History Essay (Feb 25)
179
+ - Biology Lab Exam (Feb 22)
180
+
181
+ ## Security Notes
182
+
183
+ - The upload passcode (`exam-hub-secure-2025`) protects write operations
184
+ - Change this passcode in production
185
+ - Files are stored locally in the `data/documents` directory
186
+ - The exam database is stored in `data/exams/exams.db`
187
+
188
+ ## Next Steps
189
+
190
+ 1. **Test Basic Operations**: Try the test commands above
191
+ 2. **Upload Real Documents**: Use the upload tool with your study materials
192
+ 3. **Customize Exam Data**: Add your actual exam schedule
193
+ 4. **Integrate with Web UI**: The Next.js frontend can be updated to use the same data directory
194
+
195
+ ## Support
196
+
197
+ If you encounter issues:
198
+ 1. Check the logs in Claude Desktop's Developer Tools
199
+ 2. Verify the MCP server is running manually
200
+ 3. Ensure all paths in the config are absolute paths
201
+ 4. Make sure Python and all dependencies are installed correctly
TEST_MCP_CONNECTION.md ADDED
@@ -0,0 +1,76 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Testing MCP Connection in Claude
2
+
3
+ ## Your MCP Server is Running!
4
+
5
+ The MCP server is currently running at: `http://localhost:8000/sse`
6
+
7
+ ## How to Connect in Claude
8
+
9
+ Since you're using Claude in the browser, you can test the MCP tools directly here! The server is already running in the background.
10
+
11
+ ## Test Commands to Try Right Now
12
+
13
+ Try asking me these questions to test the MCP integration:
14
+
15
+ ### 1. Basic Server Check
16
+ "Can you check what tools are available on the exam hub server?"
17
+
18
+ ### 2. List Documents
19
+ "Show me all documents in the exam hub"
20
+
21
+ ### 3. View Exams
22
+ "What exams are scheduled this week?"
23
+
24
+ ### 4. Read a File
25
+ "Read the study_tips.md file from the exam hub"
26
+
27
+ ### 5. Search Documents
28
+ "Search for PDF files in the document repository"
29
+
30
+ ### 6. Get Exam Statistics
31
+ "Show me statistics about upcoming exams"
32
+
33
+ ### 7. Upload Test (Requires Passcode)
34
+ "Upload a test file called 'hello.txt' with content 'Hello from Claude!'"
35
+ (Use passcode: exam-hub-secure-2025)
36
+
37
+ ## Server Details
38
+
39
+ - **Status**: Running ✅
40
+ - **URL**: http://localhost:8000
41
+ - **SSE Endpoint**: http://localhost:8000/sse
42
+ - **Data Directory**: E:\mpc-hackathon\data
43
+ - **Documents**: E:\mpc-hackathon\data\documents
44
+ - **Database**: E:\mpc-hackathon\data\exams\exams.db
45
+
46
+ ## Available Tools Summary
47
+
48
+ ### Document Tools (No Auth Required)
49
+ - list_all_documents
50
+ - get_folder_structure
51
+ - search_documents
52
+ - extract_pdf_text
53
+ - extract_docx_text
54
+ - read_text_file
55
+ - search_in_document
56
+ - get_document_summary
57
+
58
+ ### Document Tools (Passcode Required)
59
+ - upload_file (passcode: exam-hub-secure-2025)
60
+ - create_folder
61
+ - delete_file
62
+
63
+ ### Calendar/Exam Tools
64
+ - get_all_exams
65
+ - get_exams_this_week
66
+ - get_exams_next_week
67
+ - get_exams_by_month
68
+ - find_exam_by_subject
69
+ - get_exams_by_date
70
+ - get_upcoming_exams
71
+ - search_related_documents
72
+ - get_study_schedule
73
+ - get_exam_statistics
74
+ - get_server_info
75
+
76
+ The server is ready for testing! Just ask me to use any of these tools.
TTT_CLAUDE_DESKTOP_GUIDE.md ADDED
@@ -0,0 +1,257 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Playing Tic Tac Toe with Claude via MCP
2
+
3
+ This guide explains how to play Tic Tac Toe with Claude Desktop using the Semsoon MCP server.
4
+
5
+ ## Overview
6
+
7
+ The Tic Tac Toe game has been integrated into the **Semsoon MCP Server**. You can play directly through:
8
+ 1. **Claude Desktop** (via MCP tools)
9
+ 2. **Web UI** (browser interface)
10
+
11
+ Both interfaces connect to the same game backend, so you could even start a game in one and continue in the other!
12
+
13
+ ---
14
+
15
+ ## Playing via Claude Desktop (MCP)
16
+
17
+ ### Prerequisites
18
+
19
+ 1. **Start the Semsoon MCP Server:**
20
+ ```bash
21
+ cd backend
22
+ python mcp_server.py
23
+ ```
24
+
25
+ 2. **Ensure Claude Desktop is configured** to connect to the Semsoon MCP server (see `CLAUDE_DESKTOP_SETUP.md`)
26
+
27
+ ### How to Play
28
+
29
+ Once connected, you can play Tic Tac Toe by simply talking to Claude! Here are the available commands:
30
+
31
+ #### 1. Start a New Game
32
+ Just ask Claude to start a game:
33
+ ```
34
+ "Let's play Tic Tac Toe!"
35
+ ```
36
+
37
+ Or be more explicit:
38
+ ```
39
+ "Start a new Tic Tac Toe game"
40
+ ```
41
+
42
+ Claude will call `ttt_new_game()` and show you:
43
+ - A game ID
44
+ - The empty board
45
+ - Board position numbers (0-8)
46
+
47
+ #### 2. Make Your Move
48
+ Tell Claude where you want to place your X:
49
+ ```
50
+ "I'll take position 4" (center)
51
+ "Place my X at position 0" (top-left)
52
+ "I choose 8" (bottom-right)
53
+ ```
54
+
55
+ Claude will:
56
+ 1. Place your X at the chosen position
57
+ 2. Automatically make its own move (O)
58
+ 3. Show you the updated board
59
+ 4. Check for a winner or draw
60
+
61
+ #### 3. Continue Playing
62
+ Keep making moves until someone wins or it's a draw!
63
+
64
+ #### 4. Game Help
65
+ Ask Claude for help anytime:
66
+ ```
67
+ "How do I play Tic Tac Toe?"
68
+ "Show me the board positions"
69
+ "What are the TTT commands?"
70
+ ```
71
+
72
+ ### Board Layout
73
+
74
+ ```
75
+ 0 | 1 | 2
76
+ ---------
77
+ 3 | 4 | 5
78
+ ---------
79
+ 6 | 7 | 8
80
+ ```
81
+
82
+ - **Position 0**: Top-left
83
+ - **Position 4**: Center
84
+ - **Position 8**: Bottom-right
85
+
86
+ ---
87
+
88
+ ## Available MCP Tools
89
+
90
+ If you want to call the tools directly, here they are:
91
+
92
+ ### `ttt_new_game()`
93
+ Start a new game. Returns a game ID and initial board.
94
+
95
+ ### `ttt_make_move(game_id, position)`
96
+ Make your move and Claude responds automatically.
97
+ - `game_id`: The ID from `ttt_new_game()`
98
+ - `position`: 0-8 (see board layout above)
99
+
100
+ ### `ttt_get_game_state(game_id)`
101
+ View the current state of a game.
102
+
103
+ ### `ttt_list_games()`
104
+ List all active games.
105
+
106
+ ### `ttt_claude_move(game_id)`
107
+ Make Claude play (if it's Claude's turn).
108
+
109
+ ### `ttt_help()`
110
+ Get detailed help and instructions.
111
+
112
+ ---
113
+
114
+ ## Example Conversation
115
+
116
+ **You:** Let's play Tic Tac Toe!
117
+
118
+ **Claude:** *[Calls ttt_new_game()]*
119
+ Great! I've started a new game. You are X, and I'm O. You go first!
120
+
121
+ ```
122
+ 0 | 1 | 2
123
+ ---------
124
+ 3 | 4 | 5
125
+ ---------
126
+ 6 | 7 | 8
127
+ ```
128
+
129
+ Where would you like to place your X?
130
+
131
+ **You:** I'll take the center, position 4
132
+
133
+ **Claude:** *[Calls ttt_make_move(game_id, 4)]*
134
+ You placed X at position 4 (center). I'll place O at position 0 (top-left).
135
+
136
+ ```
137
+ O | 1 | 2
138
+ ---------
139
+ 3 | X | 5
140
+ ---------
141
+ 6 | 7 | 8
142
+ ```
143
+
144
+ Your turn! Where would you like to go?
145
+
146
+ **You:** Position 8
147
+
148
+ **Claude:** *[Calls ttt_make_move(game_id, 8)]*
149
+ You placed X at position 8. I'll block you with O at position 2!
150
+
151
+ ```
152
+ O | 1 | O
153
+ ---------
154
+ 3 | X | 5
155
+ ---------
156
+ 6 | 7 | X
157
+ ```
158
+
159
+ And so on...
160
+
161
+ ---
162
+
163
+ ## Playing via Web UI
164
+
165
+ The TTT game is also available as a desktop app in your Web OS!
166
+
167
+ 1. **Start the Web UI** (if not already running)
168
+ 2. **Click the Tic Tac Toe icon** (pink game controller) on the desktop or dock
169
+ 3. **Play directly in the window** - click any empty cell to make your move
170
+ 4. Claude (AI) will automatically respond
171
+
172
+ The web version uses the same game engine but provides a visual interface with:
173
+ - Visual board with clickable cells
174
+ - Score tracking
175
+ - New game button
176
+ - Animated moves
177
+
178
+ ---
179
+
180
+ ## Backend Architecture
181
+
182
+ ### Game State Storage
183
+ - Games are stored in-memory in `backend/ttt_game.py`
184
+ - Each game has a unique ID
185
+ - Game state includes: board, current player, winner, game status
186
+
187
+ ### AI Algorithm
188
+ Claude uses the **Minimax algorithm** with alpha-beta pruning to play optimally. This means:
189
+ - Claude will never lose if it plays perfectly
190
+ - Best you can do is draw (if you play perfectly too)
191
+ - Good luck! 🎮
192
+
193
+ ### API Endpoints (for Web UI)
194
+ - `POST /ttt/new` - Create new game
195
+ - `POST /ttt/{game_id}/move` - Make a move
196
+ - `GET /ttt/{game_id}/state` - Get game state
197
+ - `GET /ttt/games` - List all games
198
+
199
+ ---
200
+
201
+ ## Troubleshooting
202
+
203
+ ### Claude doesn't have TTT tools
204
+ 1. Make sure the Semsoon MCP server is running
205
+ 2. Check Claude Desktop config includes Semsoon server
206
+ 3. Restart Claude Desktop
207
+ 4. Try: "What tools do you have available?" to verify
208
+
209
+ ### Game not responding
210
+ 1. Check if MCP server is still running
211
+ 2. Look for errors in server console
212
+ 3. Try starting a new game
213
+
214
+ ### Can't find my game
215
+ - Games are stored in-memory, so they're lost if server restarts
216
+ - Use `ttt_list_games()` to see all active games
217
+
218
+ ---
219
+
220
+ ## Tips for Playing
221
+
222
+ 1. **Center first** (position 4) is often a strong opening
223
+ 2. **Watch for two in a row** - block Claude or complete your own line
224
+ 3. **Control corners** - positions 0, 2, 6, 8 are strategic
225
+ 4. **Think ahead** - Claude is using minimax, so play carefully!
226
+
227
+ ---
228
+
229
+ ## Integration Details
230
+
231
+ ### File Structure
232
+ ```
233
+ backend/
234
+ ├── mcp_server.py # Main Semsoon MCP server
235
+ ├── ttt_game.py # Game logic and AI
236
+ ├── demo_api.py # REST API endpoints
237
+ └── tools/
238
+ └── ttt_tools.py # MCP tool registrations
239
+ ```
240
+
241
+ ### How It Works
242
+ 1. You talk to Claude Desktop
243
+ 2. Claude calls MCP tools (e.g., `ttt_make_move`)
244
+ 3. Tools execute game logic in `ttt_game.py`
245
+ 4. Results returned to Claude
246
+ 5. Claude shows you the board and responds naturally
247
+
248
+ ---
249
+
250
+ ## Have Fun!
251
+
252
+ Enjoy playing Tic Tac Toe with Claude! It's a great way to:
253
+ - Take study breaks
254
+ - Test the MCP integration
255
+ - Challenge yourself against perfect AI play
256
+
257
+ Remember: You're X, Claude is O. May the best player win! 🏆
USE_NGROK.md ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Using ngrok for HTTPS Access
2
+
3
+ If you need an HTTPS endpoint for your MCP server:
4
+
5
+ ## 1. Install ngrok
6
+ Download from: https://ngrok.com/download
7
+
8
+ ## 2. Start your MCP server
9
+ ```bash
10
+ cd backend
11
+ python mcp_server.py
12
+ ```
13
+
14
+ ## 3. Create HTTPS tunnel
15
+ ```bash
16
+ ngrok http 8000
17
+ ```
18
+
19
+ This will give you a public HTTPS URL like:
20
+ `https://abc123.ngrok.io`
21
+
22
+ ## 4. Use the HTTPS URL
23
+ You can then use `https://abc123.ngrok.io/sse` as your MCP endpoint.
24
+
25
+ ---
26
+
27
+ **BUT REMEMBER**: Claude's web interface doesn't support custom MCP connectors. You need Claude Desktop for MCP integration!
app/api/code/execute/route.ts ADDED
@@ -0,0 +1,186 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { NextRequest, NextResponse } from 'next/server'
2
+ import { exec } from 'child_process'
3
+ import { promisify } from 'util'
4
+ import fs from 'fs/promises'
5
+ import path from 'path'
6
+ import crypto from 'crypto'
7
+
8
+ const execAsync = promisify(exec)
9
+
10
+ // Session storage (in production, use a proper database)
11
+ const sessions = new Map<string, any>()
12
+
13
+ export async function POST(request: NextRequest) {
14
+ try {
15
+ const { sessionId, language, code, timestamp } = await request.json()
16
+
17
+ if (!sessionId || !language || !code) {
18
+ return NextResponse.json(
19
+ { error: 'Missing required parameters' },
20
+ { status: 400 }
21
+ )
22
+ }
23
+
24
+ // Create a temporary directory for this execution
25
+ const tempDir = path.join(process.cwd(), 'temp', sessionId)
26
+ await fs.mkdir(tempDir, { recursive: true })
27
+
28
+ let output = ''
29
+ let error = null
30
+
31
+ try {
32
+ switch (language) {
33
+ case 'python': {
34
+ const fileName = path.join(tempDir, `script_${timestamp}.py`)
35
+ await fs.writeFile(fileName, code)
36
+
37
+ try {
38
+ const result = await execAsync(`python "${fileName}"`, {
39
+ timeout: 10000, // 10 second timeout
40
+ maxBuffer: 1024 * 1024 // 1MB buffer
41
+ })
42
+ output = result.stdout
43
+ if (result.stderr) {
44
+ error = result.stderr
45
+ }
46
+ } catch (execError: any) {
47
+ error = execError.message || 'Execution failed'
48
+ output = execError.stdout || ''
49
+ }
50
+
51
+ // Clean up
52
+ await fs.unlink(fileName).catch(() => {})
53
+ break
54
+ }
55
+
56
+ case 'javascript':
57
+ case 'typescript': {
58
+ const fileName = path.join(tempDir, `script_${timestamp}.${language === 'typescript' ? 'ts' : 'js'}`)
59
+ await fs.writeFile(fileName, code)
60
+
61
+ try {
62
+ const command = language === 'typescript'
63
+ ? `npx ts-node "${fileName}"`
64
+ : `node "${fileName}"`
65
+
66
+ const result = await execAsync(command, {
67
+ timeout: 10000,
68
+ maxBuffer: 1024 * 1024
69
+ })
70
+ output = result.stdout
71
+ if (result.stderr) {
72
+ error = result.stderr
73
+ }
74
+ } catch (execError: any) {
75
+ error = execError.message || 'Execution failed'
76
+ output = execError.stdout || ''
77
+ }
78
+
79
+ await fs.unlink(fileName).catch(() => {})
80
+ break
81
+ }
82
+
83
+ case 'react':
84
+ case 'flutter': {
85
+ // For React and Flutter, we'll return a message since they need build processes
86
+ output = `${language === 'react' ? 'React' : 'Flutter'} code saved successfully.
87
+ To run ${language} applications, use the integrated development server.`
88
+
89
+ // Save the code for later use
90
+ const fileName = path.join(tempDir, `app_${timestamp}.${language === 'react' ? 'jsx' : 'dart'}`)
91
+ await fs.writeFile(fileName, code)
92
+ break
93
+ }
94
+
95
+ case 'html':
96
+ case 'css': {
97
+ // HTML and CSS don't execute, just save them
98
+ const extension = language === 'html' ? 'html' : 'css'
99
+ const fileName = path.join(tempDir, `file_${timestamp}.${extension}`)
100
+ await fs.writeFile(fileName, code)
101
+ output = `${language.toUpperCase()} file saved successfully. Use the preview pane to see the result.`
102
+ break
103
+ }
104
+
105
+ default:
106
+ error = `Unsupported language: ${language}`
107
+ }
108
+
109
+ // Store in session
110
+ if (!sessions.has(sessionId)) {
111
+ sessions.set(sessionId, {
112
+ files: [],
113
+ executions: []
114
+ })
115
+ }
116
+
117
+ const session = sessions.get(sessionId)
118
+ session.executions.push({
119
+ language,
120
+ code,
121
+ output,
122
+ error,
123
+ timestamp
124
+ })
125
+
126
+ // Keep only last 50 executions
127
+ if (session.executions.length > 50) {
128
+ session.executions = session.executions.slice(-50)
129
+ }
130
+
131
+ return NextResponse.json({
132
+ output,
133
+ error,
134
+ timestamp,
135
+ sessionId
136
+ })
137
+
138
+ } catch (err: any) {
139
+ return NextResponse.json(
140
+ { error: err.message || 'Execution failed' },
141
+ { status: 500 }
142
+ )
143
+ } finally {
144
+ // Clean up temp directory after some time
145
+ setTimeout(async () => {
146
+ try {
147
+ await fs.rmdir(tempDir, { recursive: true })
148
+ } catch {}
149
+ }, 60000) // Clean after 1 minute
150
+ }
151
+
152
+ } catch (err: any) {
153
+ return NextResponse.json(
154
+ { error: err.message || 'Request failed' },
155
+ { status: 500 }
156
+ )
157
+ }
158
+ }
159
+
160
+ export async function GET(request: NextRequest) {
161
+ const searchParams = request.nextUrl.searchParams
162
+ const sessionId = searchParams.get('sessionId')
163
+
164
+ if (!sessionId) {
165
+ return NextResponse.json(
166
+ { error: 'Session ID required' },
167
+ { status: 400 }
168
+ )
169
+ }
170
+
171
+ const session = sessions.get(sessionId)
172
+
173
+ if (!session) {
174
+ return NextResponse.json({
175
+ sessionId,
176
+ executions: [],
177
+ files: []
178
+ })
179
+ }
180
+
181
+ return NextResponse.json({
182
+ sessionId,
183
+ executions: session.executions || [],
184
+ files: session.files || []
185
+ })
186
+ }
app/api/code/public/route.ts ADDED
@@ -0,0 +1,85 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { NextRequest, NextResponse } from 'next/server'
2
+ import fs from 'fs/promises'
3
+ import path from 'path'
4
+
5
+ const PUBLIC_DIR = path.join(process.cwd(), 'public', 'shared-code')
6
+
7
+ // Initialize public directory
8
+ async function ensurePublicDir() {
9
+ await fs.mkdir(PUBLIC_DIR, { recursive: true })
10
+ }
11
+
12
+ export async function GET(request: NextRequest) {
13
+ try {
14
+ await ensurePublicDir()
15
+
16
+ // Read all public files
17
+ const files = await fs.readdir(PUBLIC_DIR)
18
+ const publicFiles = []
19
+
20
+ for (const file of files) {
21
+ if (file.endsWith('.json')) {
22
+ try {
23
+ const content = await fs.readFile(path.join(PUBLIC_DIR, file), 'utf-8')
24
+ const fileData = JSON.parse(content)
25
+ publicFiles.push(fileData)
26
+ } catch (err) {
27
+ console.error(`Error reading file ${file}:`, err)
28
+ }
29
+ }
30
+ }
31
+
32
+ // Sort by timestamp (newest first)
33
+ publicFiles.sort((a, b) => (b.timestamp || 0) - (a.timestamp || 0))
34
+
35
+ return NextResponse.json(publicFiles)
36
+ } catch (err: any) {
37
+ return NextResponse.json(
38
+ { error: 'Failed to load public files' },
39
+ { status: 500 }
40
+ )
41
+ }
42
+ }
43
+
44
+ export async function POST(request: NextRequest) {
45
+ try {
46
+ const { sessionId, file } = await request.json()
47
+
48
+ if (!sessionId || !file) {
49
+ return NextResponse.json(
50
+ { error: 'Missing required parameters' },
51
+ { status: 400 }
52
+ )
53
+ }
54
+
55
+ await ensurePublicDir()
56
+
57
+ // Add metadata
58
+ const publicFile = {
59
+ ...file,
60
+ id: `public_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
61
+ timestamp: Date.now(),
62
+ author: sessionId,
63
+ downloads: 0,
64
+ likes: 0
65
+ }
66
+
67
+ // Save to public directory
68
+ const fileName = `${publicFile.id}.json`
69
+ const filePath = path.join(PUBLIC_DIR, fileName)
70
+
71
+ await fs.writeFile(filePath, JSON.stringify(publicFile, null, 2))
72
+
73
+ return NextResponse.json({
74
+ success: true,
75
+ file: publicFile,
76
+ path: `/shared-code/${fileName}`
77
+ })
78
+
79
+ } catch (err: any) {
80
+ return NextResponse.json(
81
+ { error: err.message || 'Failed to save public file' },
82
+ { status: 500 }
83
+ )
84
+ }
85
+ }
app/api/code/save/route.ts ADDED
@@ -0,0 +1,130 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { NextRequest, NextResponse } from 'next/server'
2
+ import fs from 'fs'
3
+ import path from 'path'
4
+
5
+ export async function POST(request: NextRequest) {
6
+ try {
7
+ const body = await request.json()
8
+ const { sessionId, code, timestamp } = body
9
+
10
+ // Define the base path for saved code (accessible by MCP)
11
+ const baseDir = path.join(process.cwd(), 'data', 'vscode_sessions')
12
+
13
+ // Create directory if it doesn't exist
14
+ if (!fs.existsSync(baseDir)) {
15
+ fs.mkdirSync(baseDir, { recursive: true })
16
+ }
17
+
18
+ // Create session directory
19
+ const sessionDir = path.join(baseDir, sessionId)
20
+ if (!fs.existsSync(sessionDir)) {
21
+ fs.mkdirSync(sessionDir, { recursive: true })
22
+ }
23
+
24
+ // Save each file
25
+ for (const file of code) {
26
+ const filePath = path.join(sessionDir, file.name)
27
+ fs.writeFileSync(filePath, file.content, 'utf-8')
28
+ }
29
+
30
+ // Create metadata file
31
+ const metadata = {
32
+ sessionId,
33
+ timestamp,
34
+ files: code.map((f: any) => ({
35
+ name: f.name,
36
+ language: f.language,
37
+ size: f.content.length
38
+ })),
39
+ created: new Date().toISOString()
40
+ }
41
+
42
+ fs.writeFileSync(
43
+ path.join(sessionDir, 'metadata.json'),
44
+ JSON.stringify(metadata, null, 2),
45
+ 'utf-8'
46
+ )
47
+
48
+ return NextResponse.json({
49
+ success: true,
50
+ message: 'Code saved successfully',
51
+ path: sessionDir,
52
+ sessionId
53
+ })
54
+ } catch (error) {
55
+ console.error('Error saving code:', error)
56
+ return NextResponse.json(
57
+ { success: false, error: 'Failed to save code' },
58
+ { status: 500 }
59
+ )
60
+ }
61
+ }
62
+
63
+ export async function GET(request: NextRequest) {
64
+ try {
65
+ const searchParams = request.nextUrl.searchParams
66
+ const sessionId = searchParams.get('sessionId')
67
+
68
+ if (!sessionId) {
69
+ // List all sessions
70
+ const baseDir = path.join(process.cwd(), 'data', 'vscode_sessions')
71
+
72
+ if (!fs.existsSync(baseDir)) {
73
+ return NextResponse.json({ sessions: [] })
74
+ }
75
+
76
+ const sessions = fs.readdirSync(baseDir)
77
+ .filter(dir => fs.statSync(path.join(baseDir, dir)).isDirectory())
78
+ .map(dir => {
79
+ const metadataPath = path.join(baseDir, dir, 'metadata.json')
80
+ if (fs.existsSync(metadataPath)) {
81
+ const metadata = JSON.parse(fs.readFileSync(metadataPath, 'utf-8'))
82
+ return {
83
+ sessionId: dir,
84
+ ...metadata
85
+ }
86
+ }
87
+ return { sessionId: dir }
88
+ })
89
+
90
+ return NextResponse.json({ sessions })
91
+ }
92
+
93
+ // Get specific session
94
+ const sessionDir = path.join(process.cwd(), 'data', 'vscode_sessions', sessionId)
95
+
96
+ if (!fs.existsSync(sessionDir)) {
97
+ return NextResponse.json(
98
+ { error: 'Session not found' },
99
+ { status: 404 }
100
+ )
101
+ }
102
+
103
+ const files = fs.readdirSync(sessionDir)
104
+ .filter(file => file !== 'metadata.json')
105
+ .map(filename => {
106
+ const content = fs.readFileSync(path.join(sessionDir, filename), 'utf-8')
107
+ return {
108
+ name: filename,
109
+ content
110
+ }
111
+ })
112
+
113
+ const metadataPath = path.join(sessionDir, 'metadata.json')
114
+ const metadata = fs.existsSync(metadataPath)
115
+ ? JSON.parse(fs.readFileSync(metadataPath, 'utf-8'))
116
+ : {}
117
+
118
+ return NextResponse.json({
119
+ sessionId,
120
+ files,
121
+ metadata
122
+ })
123
+ } catch (error) {
124
+ console.error('Error reading code:', error)
125
+ return NextResponse.json(
126
+ { error: 'Failed to read code' },
127
+ { status: 500 }
128
+ )
129
+ }
130
+ }
app/api/download/route.ts ADDED
@@ -0,0 +1,99 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { NextRequest, NextResponse } from 'next/server'
2
+ import fs from 'fs'
3
+ import path from 'path'
4
+
5
+ const DATA_DIR = path.join(process.cwd(), 'data')
6
+ const DOCS_DIR = path.join(DATA_DIR, 'documents')
7
+
8
+ export async function GET(request: NextRequest) {
9
+ try {
10
+ const searchParams = request.nextUrl.searchParams
11
+ const filePath = searchParams.get('path')
12
+ const preview = searchParams.get('preview') === 'true'
13
+
14
+ if (!filePath) {
15
+ return NextResponse.json({ error: 'File path required' }, { status: 400 })
16
+ }
17
+
18
+ const fullPath = path.join(DOCS_DIR, filePath)
19
+
20
+ // Security check
21
+ if (!fullPath.startsWith(DOCS_DIR)) {
22
+ return NextResponse.json({ error: 'Invalid path' }, { status: 400 })
23
+ }
24
+
25
+ if (!fs.existsSync(fullPath)) {
26
+ return NextResponse.json({ error: 'File not found' }, { status: 404 })
27
+ }
28
+
29
+ const stats = fs.statSync(fullPath)
30
+ if (stats.isDirectory()) {
31
+ return NextResponse.json({ error: 'Cannot download directory' }, { status: 400 })
32
+ }
33
+
34
+ const fileBuffer = fs.readFileSync(fullPath)
35
+ const fileName = path.basename(filePath)
36
+ const ext = path.extname(fileName).toLowerCase()
37
+
38
+ // Determine content type
39
+ let contentType = 'application/octet-stream'
40
+ const mimeTypes: Record<string, string> = {
41
+ '.pdf': 'application/pdf',
42
+ '.doc': 'application/msword',
43
+ '.docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
44
+ '.xls': 'application/vnd.ms-excel',
45
+ '.xlsx': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
46
+ '.ppt': 'application/vnd.ms-powerpoint',
47
+ '.pptx': 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
48
+ '.txt': 'text/plain',
49
+ '.md': 'text/markdown',
50
+ '.json': 'application/json',
51
+ '.html': 'text/html',
52
+ '.css': 'text/css',
53
+ '.js': 'text/javascript',
54
+ '.ts': 'text/typescript',
55
+ '.py': 'text/x-python',
56
+ '.java': 'text/x-java',
57
+ '.cpp': 'text/x-c++',
58
+ '.jpg': 'image/jpeg',
59
+ '.jpeg': 'image/jpeg',
60
+ '.png': 'image/png',
61
+ '.gif': 'image/gif',
62
+ '.svg': 'image/svg+xml',
63
+ '.mp3': 'audio/mpeg',
64
+ '.mp4': 'video/mp4',
65
+ '.zip': 'application/zip',
66
+ '.rar': 'application/x-rar-compressed'
67
+ }
68
+
69
+ if (mimeTypes[ext]) {
70
+ contentType = mimeTypes[ext]
71
+ }
72
+
73
+ const headers = new Headers({
74
+ 'Content-Type': contentType,
75
+ 'Content-Length': fileBuffer.length.toString(),
76
+ })
77
+
78
+ // If not preview mode, add download header
79
+ if (!preview) {
80
+ headers.set('Content-Disposition', `attachment; filename="${fileName}"`)
81
+ } else {
82
+ // For preview, use inline disposition for supported types
83
+ if (['application/pdf', 'text/plain', 'text/markdown', 'application/json'].includes(contentType) ||
84
+ contentType.startsWith('image/') || contentType.startsWith('text/')) {
85
+ headers.set('Content-Disposition', `inline; filename="${fileName}"`)
86
+ } else {
87
+ headers.set('Content-Disposition', `attachment; filename="${fileName}"`)
88
+ }
89
+ }
90
+
91
+ return new NextResponse(fileBuffer, { headers })
92
+ } catch (error) {
93
+ console.error('Error downloading file:', error)
94
+ return NextResponse.json(
95
+ { error: 'Failed to download file' },
96
+ { status: 500 }
97
+ )
98
+ }
99
+ }
app/api/files/route.ts ADDED
@@ -0,0 +1,183 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { NextRequest, NextResponse } from 'next/server'
2
+ import fs from 'fs'
3
+ import path from 'path'
4
+
5
+ const DATA_DIR = path.join(process.cwd(), 'data')
6
+ const DOCS_DIR = path.join(DATA_DIR, 'documents')
7
+
8
+ // Ensure directories exist
9
+ if (!fs.existsSync(DATA_DIR)) {
10
+ fs.mkdirSync(DATA_DIR, { recursive: true })
11
+ }
12
+ if (!fs.existsSync(DOCS_DIR)) {
13
+ fs.mkdirSync(DOCS_DIR, { recursive: true })
14
+ }
15
+
16
+ interface FileItem {
17
+ name: string
18
+ type: 'file' | 'folder'
19
+ size?: number
20
+ modified?: string
21
+ path: string
22
+ extension?: string
23
+ }
24
+
25
+ function getFileExtension(filename: string): string {
26
+ const ext = path.extname(filename).toLowerCase()
27
+ return ext.startsWith('.') ? ext.substring(1) : ext
28
+ }
29
+
30
+ function getFilesRecursively(dir: string, basePath: string = ''): FileItem[] {
31
+ const files: FileItem[] = []
32
+
33
+ try {
34
+ const items = fs.readdirSync(dir)
35
+
36
+ for (const item of items) {
37
+ // Skip hidden files and system files
38
+ if (item.startsWith('.') || item === 'exams.db') continue
39
+
40
+ const fullPath = path.join(dir, item)
41
+ const relativePath = path.join(basePath, item).replace(/\\/g, '/')
42
+ const stats = fs.statSync(fullPath)
43
+
44
+ if (stats.isDirectory()) {
45
+ files.push({
46
+ name: item,
47
+ type: 'folder',
48
+ path: relativePath,
49
+ modified: stats.mtime.toISOString()
50
+ })
51
+ // Recursively get files from subdirectories
52
+ const subFiles = getFilesRecursively(fullPath, relativePath)
53
+ files.push(...subFiles)
54
+ } else {
55
+ files.push({
56
+ name: item,
57
+ type: 'file',
58
+ size: stats.size,
59
+ modified: stats.mtime.toISOString(),
60
+ path: relativePath,
61
+ extension: getFileExtension(item)
62
+ })
63
+ }
64
+ }
65
+ } catch (error) {
66
+ console.error('Error reading directory:', error)
67
+ }
68
+
69
+ return files
70
+ }
71
+
72
+ export async function GET(request: NextRequest) {
73
+ const searchParams = request.nextUrl.searchParams
74
+ const folder = searchParams.get('folder') || ''
75
+
76
+ try {
77
+ const targetDir = path.join(DOCS_DIR, folder)
78
+
79
+ // Security check - prevent directory traversal
80
+ if (!targetDir.startsWith(DOCS_DIR)) {
81
+ return NextResponse.json({ error: 'Invalid path' }, { status: 400 })
82
+ }
83
+
84
+ if (!fs.existsSync(targetDir)) {
85
+ // If directory doesn't exist, create it
86
+ fs.mkdirSync(targetDir, { recursive: true })
87
+ }
88
+
89
+ const files = getFilesRecursively(targetDir, folder)
90
+
91
+ return NextResponse.json({
92
+ files,
93
+ currentPath: folder,
94
+ dataDir: DATA_DIR
95
+ })
96
+ } catch (error) {
97
+ console.error('Error listing files:', error)
98
+ return NextResponse.json(
99
+ { error: 'Failed to list files' },
100
+ { status: 500 }
101
+ )
102
+ }
103
+ }
104
+
105
+ // Create folder endpoint
106
+ export async function POST(request: NextRequest) {
107
+ try {
108
+ const body = await request.json()
109
+ const { folderName, parentPath = '' } = body
110
+
111
+ if (!folderName) {
112
+ return NextResponse.json({ error: 'Folder name required' }, { status: 400 })
113
+ }
114
+
115
+ const folderPath = path.join(DOCS_DIR, parentPath, folderName)
116
+
117
+ // Security check
118
+ if (!folderPath.startsWith(DOCS_DIR)) {
119
+ return NextResponse.json({ error: 'Invalid path' }, { status: 400 })
120
+ }
121
+
122
+ if (fs.existsSync(folderPath)) {
123
+ return NextResponse.json({ error: 'Folder already exists' }, { status: 400 })
124
+ }
125
+
126
+ fs.mkdirSync(folderPath, { recursive: true })
127
+
128
+ return NextResponse.json({
129
+ success: true,
130
+ path: path.join(parentPath, folderName).replace(/\\/g, '/')
131
+ })
132
+ } catch (error) {
133
+ console.error('Error creating folder:', error)
134
+ return NextResponse.json(
135
+ { error: 'Failed to create folder' },
136
+ { status: 500 }
137
+ )
138
+ }
139
+ }
140
+
141
+ // Delete file endpoint
142
+ export async function DELETE(request: NextRequest) {
143
+ try {
144
+ const searchParams = request.nextUrl.searchParams
145
+ const filePath = searchParams.get('path')
146
+
147
+ if (!filePath) {
148
+ return NextResponse.json({ error: 'File path required' }, { status: 400 })
149
+ }
150
+
151
+ const fullPath = path.join(DOCS_DIR, filePath)
152
+
153
+ // Security check
154
+ if (!fullPath.startsWith(DOCS_DIR)) {
155
+ return NextResponse.json({ error: 'Invalid path' }, { status: 400 })
156
+ }
157
+
158
+ if (!fs.existsSync(fullPath)) {
159
+ return NextResponse.json({ error: 'File not found' }, { status: 404 })
160
+ }
161
+
162
+ // Move to trash instead of permanent delete
163
+ const trashDir = path.join(DATA_DIR, '.trash')
164
+ if (!fs.existsSync(trashDir)) {
165
+ fs.mkdirSync(trashDir, { recursive: true })
166
+ }
167
+
168
+ const timestamp = Date.now()
169
+ const trashPath = path.join(trashDir, `${timestamp}_${path.basename(filePath)}`)
170
+ fs.renameSync(fullPath, trashPath)
171
+
172
+ return NextResponse.json({
173
+ success: true,
174
+ message: 'File moved to trash'
175
+ })
176
+ } catch (error) {
177
+ console.error('Error deleting file:', error)
178
+ return NextResponse.json(
179
+ { error: 'Failed to delete file' },
180
+ { status: 500 }
181
+ )
182
+ }
183
+ }
app/api/gemini/chat/route.ts ADDED
@@ -0,0 +1,113 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { NextRequest, NextResponse } from 'next/server'
2
+
3
+ const GEMINI_API_URL = 'https://generativelanguage.googleapis.com/v1beta/models/gemini-1.5-flash:generateContent'
4
+ const GEMINI_API_KEY = process.env.GEMINI_API_KEY
5
+
6
+ export async function POST(request: NextRequest) {
7
+ try {
8
+ const { message, imageUrl, history, generateImage } = await request.json()
9
+
10
+ if (!GEMINI_API_KEY) {
11
+ return NextResponse.json(
12
+ { error: 'Gemini API key not configured on server. Please set GEMINI_API_KEY environment variable.' },
13
+ { status: 500 }
14
+ )
15
+ }
16
+
17
+ // For image generation requests
18
+ if (generateImage) {
19
+ // Note: Gemini doesn't directly generate images, but we can use a prompt to describe what an image should contain
20
+ // You would typically integrate with an image generation service like Stable Diffusion or DALL-E here
21
+ return NextResponse.json({
22
+ 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.`,
23
+ imageDescription: message,
24
+ note: 'Image generation requires integration with specialized services.'
25
+ })
26
+ }
27
+
28
+ // Prepare the request body for Gemini API
29
+ const requestBody: any = {
30
+ contents: [],
31
+ generationConfig: {
32
+ temperature: 0.9,
33
+ topK: 1,
34
+ topP: 1,
35
+ maxOutputTokens: 2048,
36
+ }
37
+ }
38
+
39
+ // Add conversation history if available
40
+ if (history && history.length > 0) {
41
+ history.forEach((msg: any) => {
42
+ requestBody.contents.push({
43
+ role: msg.role === 'user' ? 'user' : 'model',
44
+ parts: [{ text: msg.content }]
45
+ })
46
+ })
47
+ }
48
+
49
+ // Prepare the current message
50
+ const parts: any[] = []
51
+
52
+ if (message) {
53
+ parts.push({ text: message })
54
+ }
55
+
56
+ if (imageUrl) {
57
+ // Extract base64 data from data URL
58
+ const base64Data = imageUrl.split(',')[1]
59
+ parts.push({
60
+ inline_data: {
61
+ mime_type: imageUrl.split(':')[1].split(';')[0],
62
+ data: base64Data
63
+ }
64
+ })
65
+ }
66
+
67
+ requestBody.contents.push({
68
+ role: 'user',
69
+ parts: parts
70
+ })
71
+
72
+ // Make request to Gemini API
73
+ const response = await fetch(`${GEMINI_API_URL}?key=${GEMINI_API_KEY}`, {
74
+ method: 'POST',
75
+ headers: {
76
+ 'Content-Type': 'application/json',
77
+ },
78
+ body: JSON.stringify(requestBody)
79
+ })
80
+
81
+ if (!response.ok) {
82
+ const errorText = await response.text()
83
+ console.error('Gemini API error:', errorText)
84
+ return NextResponse.json(
85
+ { error: 'Failed to get response from Gemini', details: errorText },
86
+ { status: response.status }
87
+ )
88
+ }
89
+
90
+ const data = await response.json()
91
+
92
+ if (!data.candidates || data.candidates.length === 0) {
93
+ return NextResponse.json(
94
+ { error: 'No response generated' },
95
+ { status: 500 }
96
+ )
97
+ }
98
+
99
+ const generatedText = data.candidates[0].content.parts[0].text
100
+
101
+ return NextResponse.json({
102
+ response: generatedText
103
+ })
104
+
105
+ } catch (error) {
106
+ console.error('Error in Gemini chat API:', error)
107
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred'
108
+ return NextResponse.json(
109
+ { error: 'Failed to process request', details: errorMessage },
110
+ { status: 500 }
111
+ )
112
+ }
113
+ }
app/api/gemini/transcribe/route.ts ADDED
@@ -0,0 +1,94 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { NextRequest, NextResponse } from 'next/server'
2
+
3
+ // Note: For audio transcription, we'll use Gemini's multimodal capabilities
4
+ // In production, you might want to use Google Cloud Speech-to-Text API for better accuracy
5
+
6
+ export async function POST(request: NextRequest) {
7
+ try {
8
+ const formData = await request.formData()
9
+ const audioFile = formData.get('audio') as File
10
+ const apiKey = formData.get('apiKey') as string
11
+
12
+ if (!apiKey) {
13
+ return NextResponse.json(
14
+ { error: 'API key is required' },
15
+ { status: 400 }
16
+ )
17
+ }
18
+
19
+ if (!audioFile) {
20
+ return NextResponse.json(
21
+ { error: 'Audio file is required' },
22
+ { status: 400 }
23
+ )
24
+ }
25
+
26
+ // Convert audio file to base64
27
+ const bytes = await audioFile.arrayBuffer()
28
+ const buffer = Buffer.from(bytes)
29
+ const base64Audio = buffer.toString('base64')
30
+
31
+ // Use Gemini API to transcribe
32
+ // Note: Gemini 1.5 Pro supports audio, but Flash might have limitations
33
+ const GEMINI_API_URL = 'https://generativelanguage.googleapis.com/v1beta/models/gemini-1.5-flash:generateContent'
34
+
35
+ const requestBody = {
36
+ contents: [{
37
+ role: 'user',
38
+ parts: [
39
+ {
40
+ text: 'Please transcribe the following audio accurately. Only return the transcription text, nothing else.'
41
+ },
42
+ {
43
+ inline_data: {
44
+ mime_type: audioFile.type || 'audio/wav',
45
+ data: base64Audio
46
+ }
47
+ }
48
+ ]
49
+ }],
50
+ generationConfig: {
51
+ temperature: 0.1,
52
+ topK: 1,
53
+ topP: 1,
54
+ maxOutputTokens: 2048,
55
+ }
56
+ }
57
+
58
+ const response = await fetch(`${GEMINI_API_URL}?key=${apiKey}`, {
59
+ method: 'POST',
60
+ headers: {
61
+ 'Content-Type': 'application/json',
62
+ },
63
+ body: JSON.stringify(requestBody)
64
+ })
65
+
66
+ if (!response.ok) {
67
+ const error = await response.json()
68
+
69
+ // If Gemini doesn't support audio, provide alternative solution
70
+ if (error.error?.message?.includes('audio') || error.error?.message?.includes('unsupported')) {
71
+ return NextResponse.json({
72
+ 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.]',
73
+ warning: 'Audio transcription not fully supported with current model'
74
+ })
75
+ }
76
+
77
+ throw new Error(error.error?.message || 'Failed to transcribe audio')
78
+ }
79
+
80
+ const data = await response.json()
81
+ const transcription = data.candidates?.[0]?.content?.parts?.[0]?.text || 'Could not transcribe audio'
82
+
83
+ return NextResponse.json({ transcription })
84
+
85
+ } catch (error) {
86
+ console.error('Transcription error:', error)
87
+
88
+ // Provide a helpful fallback message
89
+ return NextResponse.json({
90
+ transcription: '',
91
+ error: error instanceof Error ? error.message : 'Transcription failed. Note: Audio transcription requires Gemini 1.5 Pro or a dedicated speech-to-text API.'
92
+ }, { status: 500 })
93
+ }
94
+ }
app/api/public/route.ts ADDED
@@ -0,0 +1,178 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { NextRequest, NextResponse } from 'next/server'
2
+ import fs from 'fs'
3
+ import path from 'path'
4
+
5
+ const DATA_DIR = path.join(process.cwd(), 'data')
6
+ const PUBLIC_DIR = path.join(DATA_DIR, 'public')
7
+
8
+ // Ensure public directory exists
9
+ if (!fs.existsSync(PUBLIC_DIR)) {
10
+ fs.mkdirSync(PUBLIC_DIR, { recursive: true })
11
+ }
12
+
13
+ interface FileItem {
14
+ name: string
15
+ type: 'file' | 'folder'
16
+ size?: number
17
+ modified?: string
18
+ path: string
19
+ extension?: string
20
+ uploadedBy?: string
21
+ uploadedAt?: string
22
+ }
23
+
24
+ function getFileExtension(filename: string): string {
25
+ const ext = path.extname(filename).toLowerCase()
26
+ return ext.startsWith('.') ? ext.substring(1) : ext
27
+ }
28
+
29
+ export async function GET(request: NextRequest) {
30
+ const searchParams = request.nextUrl.searchParams
31
+ const folder = searchParams.get('folder') || ''
32
+
33
+ try {
34
+ const targetDir = path.join(PUBLIC_DIR, folder)
35
+
36
+ // Security check - prevent directory traversal
37
+ if (!targetDir.startsWith(PUBLIC_DIR)) {
38
+ return NextResponse.json({ error: 'Invalid path' }, { status: 400 })
39
+ }
40
+
41
+ if (!fs.existsSync(targetDir)) {
42
+ fs.mkdirSync(targetDir, { recursive: true })
43
+ }
44
+
45
+ const files: FileItem[] = []
46
+ const items = fs.readdirSync(targetDir)
47
+
48
+ for (const item of items) {
49
+ // Skip hidden files
50
+ if (item.startsWith('.')) continue
51
+
52
+ const fullPath = path.join(targetDir, item)
53
+ const relativePath = path.join(folder, item).replace(/\\/g, '/')
54
+ const stats = fs.statSync(fullPath)
55
+
56
+ if (stats.isDirectory()) {
57
+ files.push({
58
+ name: item,
59
+ type: 'folder',
60
+ path: relativePath,
61
+ modified: stats.mtime.toISOString()
62
+ })
63
+ } else {
64
+ // Try to read metadata if it exists
65
+ const metadataPath = fullPath + '.meta.json'
66
+ let metadata: any = {}
67
+ if (fs.existsSync(metadataPath)) {
68
+ try {
69
+ metadata = JSON.parse(fs.readFileSync(metadataPath, 'utf-8'))
70
+ } catch (e) {
71
+ // Ignore metadata errors
72
+ }
73
+ }
74
+
75
+ files.push({
76
+ name: item,
77
+ type: 'file',
78
+ size: stats.size,
79
+ modified: stats.mtime.toISOString(),
80
+ path: relativePath,
81
+ extension: getFileExtension(item),
82
+ uploadedBy: metadata.uploadedBy || 'Anonymous',
83
+ uploadedAt: metadata.uploadedAt || stats.birthtime.toISOString()
84
+ })
85
+ }
86
+ }
87
+
88
+ return NextResponse.json({
89
+ files,
90
+ currentPath: folder,
91
+ isPublic: true
92
+ })
93
+ } catch (error) {
94
+ console.error('Error listing public files:', error)
95
+ return NextResponse.json(
96
+ { error: 'Failed to list public files' },
97
+ { status: 500 }
98
+ )
99
+ }
100
+ }
101
+
102
+ // Upload to public folder
103
+ export async function POST(request: NextRequest) {
104
+ try {
105
+ const formData = await request.formData()
106
+ const file = formData.get('file') as File
107
+ const folder = formData.get('folder') as string || ''
108
+ const uploadedBy = formData.get('uploadedBy') as string || 'Anonymous'
109
+
110
+ if (!file) {
111
+ return NextResponse.json({ error: 'No file provided' }, { status: 400 })
112
+ }
113
+
114
+ const targetDir = path.join(PUBLIC_DIR, folder)
115
+
116
+ // Security check
117
+ if (!targetDir.startsWith(PUBLIC_DIR)) {
118
+ return NextResponse.json({ error: 'Invalid path' }, { status: 400 })
119
+ }
120
+
121
+ if (!fs.existsSync(targetDir)) {
122
+ fs.mkdirSync(targetDir, { recursive: true })
123
+ }
124
+
125
+ const fileName = file.name
126
+ const filePath = path.join(targetDir, fileName)
127
+
128
+ // Check if file already exists
129
+ if (fs.existsSync(filePath)) {
130
+ // Add timestamp to filename
131
+ const timestamp = Date.now()
132
+ const ext = path.extname(fileName)
133
+ const baseName = path.basename(fileName, ext)
134
+ const newFileName = `${baseName}_${timestamp}${ext}`
135
+ const newFilePath = path.join(targetDir, newFileName)
136
+
137
+ const buffer = Buffer.from(await file.arrayBuffer())
138
+ fs.writeFileSync(newFilePath, buffer)
139
+
140
+ // Save metadata
141
+ const metadataPath = newFilePath + '.meta.json'
142
+ fs.writeFileSync(metadataPath, JSON.stringify({
143
+ uploadedBy,
144
+ uploadedAt: new Date().toISOString(),
145
+ originalName: fileName
146
+ }))
147
+
148
+ return NextResponse.json({
149
+ success: true,
150
+ message: 'File uploaded to public folder',
151
+ path: path.join(folder, newFileName).replace(/\\/g, '/'),
152
+ renamed: true
153
+ })
154
+ } else {
155
+ const buffer = Buffer.from(await file.arrayBuffer())
156
+ fs.writeFileSync(filePath, buffer)
157
+
158
+ // Save metadata
159
+ const metadataPath = filePath + '.meta.json'
160
+ fs.writeFileSync(metadataPath, JSON.stringify({
161
+ uploadedBy,
162
+ uploadedAt: new Date().toISOString()
163
+ }))
164
+
165
+ return NextResponse.json({
166
+ success: true,
167
+ message: 'File uploaded to public folder',
168
+ path: path.join(folder, fileName).replace(/\\/g, '/')
169
+ })
170
+ }
171
+ } catch (error) {
172
+ console.error('Error uploading to public folder:', error)
173
+ return NextResponse.json(
174
+ { error: 'Failed to upload file' },
175
+ { status: 500 }
176
+ )
177
+ }
178
+ }
app/api/upload/route.ts ADDED
@@ -0,0 +1,84 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { NextRequest, NextResponse } from 'next/server'
2
+ import fs from 'fs'
3
+ import path from 'path'
4
+
5
+ const DATA_DIR = path.join(process.cwd(), 'data')
6
+ const DOCS_DIR = path.join(DATA_DIR, 'documents')
7
+ const MAX_FILE_SIZE = 50 * 1024 * 1024 // 50MB
8
+
9
+ export async function POST(request: NextRequest) {
10
+ try {
11
+ const formData = await request.formData()
12
+ const file = formData.get('file') as File
13
+ const folderPath = formData.get('folder') as string || ''
14
+
15
+ if (!file) {
16
+ return NextResponse.json({ error: 'No file provided' }, { status: 400 })
17
+ }
18
+
19
+ // Check file size
20
+ if (file.size > MAX_FILE_SIZE) {
21
+ return NextResponse.json(
22
+ { error: `File size exceeds ${MAX_FILE_SIZE / 1024 / 1024}MB limit` },
23
+ { status: 400 }
24
+ )
25
+ }
26
+
27
+ // Sanitize filename
28
+ const fileName = file.name.replace(/[^a-zA-Z0-9._-]/g, '_')
29
+ const uploadDir = path.join(DOCS_DIR, folderPath)
30
+ const filePath = path.join(uploadDir, fileName)
31
+
32
+ // Security check
33
+ if (!filePath.startsWith(DOCS_DIR)) {
34
+ return NextResponse.json({ error: 'Invalid upload path' }, { status: 400 })
35
+ }
36
+
37
+ // Create directory if it doesn't exist
38
+ if (!fs.existsSync(uploadDir)) {
39
+ fs.mkdirSync(uploadDir, { recursive: true })
40
+ }
41
+
42
+ // Check if file already exists
43
+ if (fs.existsSync(filePath)) {
44
+ // Add timestamp to filename if it exists
45
+ const timestamp = Date.now()
46
+ const ext = path.extname(fileName)
47
+ const baseName = path.basename(fileName, ext)
48
+ const newFileName = `${baseName}_${timestamp}${ext}`
49
+ const newFilePath = path.join(uploadDir, newFileName)
50
+
51
+ // Convert file to buffer and save
52
+ const bytes = await file.arrayBuffer()
53
+ const buffer = Buffer.from(bytes)
54
+ fs.writeFileSync(newFilePath, buffer)
55
+
56
+ return NextResponse.json({
57
+ success: true,
58
+ message: 'File uploaded (renamed due to conflict)',
59
+ fileName: newFileName,
60
+ path: path.join(folderPath, newFileName).replace(/\\/g, '/'),
61
+ size: file.size
62
+ })
63
+ }
64
+
65
+ // Convert file to buffer and save
66
+ const bytes = await file.arrayBuffer()
67
+ const buffer = Buffer.from(bytes)
68
+ fs.writeFileSync(filePath, buffer)
69
+
70
+ return NextResponse.json({
71
+ success: true,
72
+ message: 'File uploaded successfully',
73
+ fileName: fileName,
74
+ path: path.join(folderPath, fileName).replace(/\\/g, '/'),
75
+ size: file.size
76
+ })
77
+ } catch (error) {
78
+ console.error('Error uploading file:', error)
79
+ return NextResponse.json(
80
+ { error: 'Failed to upload file' },
81
+ { status: 500 }
82
+ )
83
+ }
84
+ }
app/components/AboutModal.tsx ADDED
@@ -0,0 +1,179 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ 'use client'
2
+
3
+ import React, { useEffect, useState } from 'react'
4
+ import { X, Info, HardDrive, Cpu, Globe, Key } from '@phosphor-icons/react'
5
+ import { motion, AnimatePresence } from 'framer-motion'
6
+
7
+ interface AboutModalProps {
8
+ isOpen: boolean
9
+ onClose: () => void
10
+ }
11
+
12
+ export function AboutModal({ isOpen, onClose }: AboutModalProps) {
13
+ const [systemId, setSystemId] = useState<string>('')
14
+ const [publicPath, setPublicPath] = useState<string>('')
15
+ const [tempPath, setTempPath] = useState<string>('')
16
+
17
+ useEffect(() => {
18
+ // Generate unique system ID based on browser fingerprint
19
+ const generateSystemId = () => {
20
+ const nav = window.navigator
21
+ const screen = window.screen
22
+ const fingerprint = [
23
+ nav.userAgent,
24
+ nav.language,
25
+ screen.colorDepth,
26
+ screen.width,
27
+ screen.height,
28
+ new Date().getTimezoneOffset(),
29
+ nav.hardwareConcurrency,
30
+ nav.platform
31
+ ].join('|')
32
+
33
+ // Create a simple hash
34
+ let hash = 0
35
+ for (let i = 0; i < fingerprint.length; i++) {
36
+ const char = fingerprint.charCodeAt(i)
37
+ hash = ((hash << 5) - hash) + char
38
+ hash = hash & hash // Convert to 32-bit integer
39
+ }
40
+
41
+ // Convert to hex and make it look like a system ID
42
+ const hexHash = Math.abs(hash).toString(16).toUpperCase()
43
+ return `REUBEN-${hexHash.substring(0, 4)}-${hexHash.substring(4, 8)}-${Date.now().toString(36).toUpperCase()}`
44
+ }
45
+
46
+ // Set system paths
47
+ setSystemId(generateSystemId())
48
+ setPublicPath('E:\\mpc-hackathon\\public')
49
+ setTempPath(`E:\\mpc-hackathon\\data\\temp_${Date.now()}`)
50
+ }, [])
51
+
52
+ if (!isOpen) return null
53
+
54
+ return (
55
+ <AnimatePresence>
56
+ {isOpen && (
57
+ <>
58
+ <div className="fixed inset-0 bg-black/50 z-[100]" onClick={onClose} />
59
+ <motion.div
60
+ initial={{ scale: 0.9, opacity: 0 }}
61
+ animate={{ scale: 1, opacity: 1 }}
62
+ exit={{ scale: 0.9, opacity: 0 }}
63
+ transition={{ duration: 0.2 }}
64
+ className="fixed top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 z-[101]"
65
+ >
66
+ <div className="bg-white rounded-2xl shadow-2xl p-8 min-w-[500px] max-w-[600px]">
67
+ {/* Logo and Title */}
68
+ <div className="text-center mb-6">
69
+ <div className="w-24 h-24 mx-auto mb-4 bg-gradient-to-br from-purple-600 to-blue-600 rounded-2xl flex items-center justify-center">
70
+ <span className="text-white font-bold text-3xl">R</span>
71
+ </div>
72
+ <h1 className="text-3xl font-bold text-gray-900">Reuben OS</h1>
73
+ <p className="text-gray-500 mt-2">Advanced Desktop Environment</p>
74
+ <p className="text-sm text-gray-400 mt-1">Version 1.0.0</p>
75
+ </div>
76
+
77
+ {/* System Information */}
78
+ <div className="space-y-4 mb-6">
79
+ <div className="bg-gray-50 rounded-lg p-4">
80
+ <div className="flex items-center mb-2">
81
+ <Key className="text-purple-600 mr-2" size={20} weight="fill" />
82
+ <span className="font-semibold text-gray-700">System ID</span>
83
+ </div>
84
+ <code className="text-xs bg-white px-2 py-1 rounded border border-gray-200 text-gray-600 font-mono block">
85
+ {systemId || 'Generating...'}
86
+ </code>
87
+ </div>
88
+
89
+ <div className="bg-gray-50 rounded-lg p-4">
90
+ <div className="flex items-center mb-2">
91
+ <HardDrive className="text-blue-600 mr-2" size={20} weight="fill" />
92
+ <span className="font-semibold text-gray-700">Storage Paths</span>
93
+ </div>
94
+ <div className="space-y-2">
95
+ <div>
96
+ <span className="text-xs text-gray-500">Public:</span>
97
+ <code className="text-xs bg-white px-2 py-1 rounded border border-gray-200 text-gray-600 font-mono block mt-1">
98
+ {publicPath}
99
+ </code>
100
+ </div>
101
+ <div>
102
+ <span className="text-xs text-gray-500">Temp:</span>
103
+ <code className="text-xs bg-white px-2 py-1 rounded border border-gray-200 text-gray-600 font-mono block mt-1">
104
+ {tempPath}
105
+ </code>
106
+ </div>
107
+ </div>
108
+ </div>
109
+
110
+ <div className="bg-gray-50 rounded-lg p-4">
111
+ <div className="flex items-center mb-2">
112
+ <Cpu className="text-green-600 mr-2" size={20} weight="fill" />
113
+ <span className="font-semibold text-gray-700">Capabilities</span>
114
+ </div>
115
+ <div className="text-xs text-gray-600 space-y-1">
116
+ <div className="flex items-center">
117
+ <span className="w-2 h-2 bg-green-500 rounded-full mr-2"></span>
118
+ Python Code Execution (via MCP)
119
+ </div>
120
+ <div className="flex items-center">
121
+ <span className="w-2 h-2 bg-green-500 rounded-full mr-2"></span>
122
+ HTML/CSS/JS Preview
123
+ </div>
124
+ <div className="flex items-center">
125
+ <span className="w-2 h-2 bg-green-500 rounded-full mr-2"></span>
126
+ File Management System
127
+ </div>
128
+ <div className="flex items-center">
129
+ <span className="w-2 h-2 bg-green-500 rounded-full mr-2"></span>
130
+ Claude AI Integration
131
+ </div>
132
+ <div className="flex items-center">
133
+ <span className="w-2 h-2 bg-yellow-500 rounded-full mr-2"></span>
134
+ Flutter/Dart (Coming Soon)
135
+ </div>
136
+ </div>
137
+ </div>
138
+
139
+ <div className="bg-gray-50 rounded-lg p-4">
140
+ <div className="flex items-center mb-2">
141
+ <Globe className="text-indigo-600 mr-2" size={20} weight="fill" />
142
+ <span className="font-semibold text-gray-700">MCP Server</span>
143
+ </div>
144
+ <div className="text-xs text-gray-600">
145
+ <div>Status: <span className="text-green-600 font-semibold">Connected</span></div>
146
+ <div>Endpoint: <code className="font-mono">reuben-os</code></div>
147
+ <div>Tools: 25+ available</div>
148
+ </div>
149
+ </div>
150
+ </div>
151
+
152
+ {/* Footer */}
153
+ <div className="text-center text-xs text-gray-400 mb-4">
154
+ <p>© 2024 Reuben OS. All rights reserved.</p>
155
+ <p className="mt-1">Built with Next.js, React, and MCP Technology</p>
156
+ </div>
157
+
158
+ {/* Close Button */}
159
+ <button
160
+ onClick={onClose}
161
+ className="w-full bg-gradient-to-r from-purple-600 to-blue-600 text-white py-2 rounded-lg font-semibold hover:opacity-90 transition-opacity"
162
+ >
163
+ Close
164
+ </button>
165
+
166
+ {/* X button in corner */}
167
+ <button
168
+ onClick={onClose}
169
+ className="absolute top-4 right-4 text-gray-400 hover:text-gray-600 transition-colors"
170
+ >
171
+ <X size={20} weight="bold" />
172
+ </button>
173
+ </div>
174
+ </motion.div>
175
+ </>
176
+ )}
177
+ </AnimatePresence>
178
+ )
179
+ }
app/components/BackgroundSelector.tsx ADDED
@@ -0,0 +1,217 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ 'use client'
2
+
3
+ import React, { useState, useRef } from 'react'
4
+ import { motion, AnimatePresence } from 'framer-motion'
5
+ import { X, Upload, Image, Check } from '@phosphor-icons/react'
6
+
7
+ interface BackgroundSelectorProps {
8
+ isOpen: boolean
9
+ onClose: () => void
10
+ onSelectBackground: (background: string | File) => void
11
+ currentBackground: string
12
+ }
13
+
14
+ const presetBackgrounds = [
15
+ {
16
+ id: 'gradient-purple',
17
+ name: 'Ubuntu Purple',
18
+ style: 'linear-gradient(135deg, #77216F 0%, #5E2750 50%, #2C001E 100%)'
19
+ },
20
+ {
21
+ id: 'gradient-blue',
22
+ name: 'Ocean Blue',
23
+ style: 'linear-gradient(135deg, #1e3c72 0%, #2a5298 50%, #7e8ba3 100%)'
24
+ },
25
+ {
26
+ id: 'gradient-green',
27
+ name: 'Forest Green',
28
+ style: 'linear-gradient(135deg, #134e5e 0%, #71b280 50%, #a8e063 100%)'
29
+ },
30
+ {
31
+ id: 'gradient-orange',
32
+ name: 'Sunset Orange',
33
+ style: 'linear-gradient(135deg, #ff512f 0%, #dd2476 50%, #f09819 100%)'
34
+ },
35
+ {
36
+ id: 'gradient-dark',
37
+ name: 'Dark Mode',
38
+ style: 'linear-gradient(135deg, #0f0f0f 0%, #1a1a1a 50%, #2d2d2d 100%)'
39
+ },
40
+ {
41
+ id: 'gradient-cosmic',
42
+ name: 'Cosmic',
43
+ style: 'linear-gradient(135deg, #667eea 0%, #764ba2 50%, #f093fb 100%)'
44
+ }
45
+ ]
46
+
47
+ export function BackgroundSelector({
48
+ isOpen,
49
+ onClose,
50
+ onSelectBackground,
51
+ currentBackground
52
+ }: BackgroundSelectorProps) {
53
+ const [selectedTab, setSelectedTab] = useState<'presets' | 'upload'>('presets')
54
+ const [uploadedImage, setUploadedImage] = useState<string | null>(null)
55
+ const fileInputRef = useRef<HTMLInputElement>(null)
56
+
57
+ const handleFileUpload = (event: React.ChangeEvent<HTMLInputElement>) => {
58
+ const file = event.target.files?.[0]
59
+ if (file) {
60
+ const reader = new FileReader()
61
+ reader.onload = (e) => {
62
+ const result = e.target?.result as string
63
+ setUploadedImage(result)
64
+ onSelectBackground(file)
65
+ }
66
+ reader.readAsDataURL(file)
67
+ }
68
+ }
69
+
70
+ return (
71
+ <AnimatePresence>
72
+ {isOpen && (
73
+ <>
74
+ {/* Backdrop */}
75
+ <motion.div
76
+ initial={{ opacity: 0 }}
77
+ animate={{ opacity: 1 }}
78
+ exit={{ opacity: 0 }}
79
+ onClick={onClose}
80
+ className="fixed inset-0 bg-black/50 backdrop-blur-sm z-50"
81
+ />
82
+
83
+ {/* Modal */}
84
+ <motion.div
85
+ initial={{ scale: 0.9, opacity: 0 }}
86
+ animate={{ scale: 1, opacity: 1 }}
87
+ exit={{ scale: 0.9, opacity: 0 }}
88
+ transition={{ type: "spring", damping: 20, stiffness: 300 }}
89
+ className="fixed top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-[90%] max-w-2xl bg-[#2C2C2C]/95 backdrop-blur-md rounded-xl shadow-2xl z-50 border border-white/10"
90
+ >
91
+ {/* Header */}
92
+ <div className="flex items-center justify-between p-6 border-b border-white/10">
93
+ <h2 className="text-xl font-semibold text-white">Change Desktop Background</h2>
94
+ <button
95
+ onClick={onClose}
96
+ className="text-gray-400 hover:text-white transition-colors p-1 hover:bg-white/10 rounded-lg"
97
+ >
98
+ <X size={20} />
99
+ </button>
100
+ </div>
101
+
102
+ {/* Tabs */}
103
+ <div className="flex border-b border-white/10">
104
+ <button
105
+ onClick={() => setSelectedTab('presets')}
106
+ className={`flex-1 px-4 py-3 text-sm font-medium transition-colors ${
107
+ selectedTab === 'presets'
108
+ ? 'text-white border-b-2 border-blue-500'
109
+ : 'text-gray-400 hover:text-white'
110
+ }`}
111
+ >
112
+ Gallery
113
+ </button>
114
+ <button
115
+ onClick={() => setSelectedTab('upload')}
116
+ className={`flex-1 px-4 py-3 text-sm font-medium transition-colors ${
117
+ selectedTab === 'upload'
118
+ ? 'text-white border-b-2 border-blue-500'
119
+ : 'text-gray-400 hover:text-white'
120
+ }`}
121
+ >
122
+ Upload Image
123
+ </button>
124
+ </div>
125
+
126
+ {/* Content */}
127
+ <div className="p-6">
128
+ {selectedTab === 'presets' ? (
129
+ <div className="grid grid-cols-3 gap-4">
130
+ {presetBackgrounds.map((bg) => (
131
+ <button
132
+ key={bg.id}
133
+ onClick={() => onSelectBackground(bg.id)}
134
+ className={`relative aspect-video rounded-lg overflow-hidden border-2 transition-all ${
135
+ currentBackground === bg.id
136
+ ? 'border-blue-500 scale-105'
137
+ : 'border-white/20 hover:border-white/40'
138
+ }`}
139
+ >
140
+ <div
141
+ className="w-full h-full"
142
+ style={{ background: bg.style }}
143
+ />
144
+ {currentBackground === bg.id && (
145
+ <div className="absolute top-2 right-2 w-6 h-6 bg-blue-500 rounded-full flex items-center justify-center">
146
+ <Check size={14} weight="bold" className="text-white" />
147
+ </div>
148
+ )}
149
+ <div className="absolute bottom-0 left-0 right-0 bg-black/50 backdrop-blur-sm px-2 py-1">
150
+ <span className="text-xs text-white">{bg.name}</span>
151
+ </div>
152
+ </button>
153
+ ))}
154
+ </div>
155
+ ) : (
156
+ <div className="flex flex-col items-center justify-center py-12">
157
+ <input
158
+ ref={fileInputRef}
159
+ type="file"
160
+ accept="image/*"
161
+ onChange={handleFileUpload}
162
+ className="hidden"
163
+ />
164
+
165
+ {uploadedImage ? (
166
+ <div className="relative w-full max-w-md">
167
+ <img
168
+ src={uploadedImage}
169
+ alt="Uploaded background"
170
+ className="w-full h-48 object-cover rounded-lg"
171
+ />
172
+ <button
173
+ onClick={() => fileInputRef.current?.click()}
174
+ className="mt-4 w-full px-4 py-2 bg-blue-500 hover:bg-blue-600 text-white rounded-lg transition-colors"
175
+ >
176
+ Choose Different Image
177
+ </button>
178
+ </div>
179
+ ) : (
180
+ <>
181
+ <div
182
+ onClick={() => fileInputRef.current?.click()}
183
+ 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"
184
+ >
185
+ <Upload size={32} className="text-gray-400 mb-2" />
186
+ <span className="text-sm text-gray-400">Click to upload</span>
187
+ </div>
188
+ <p className="mt-4 text-sm text-gray-400">
189
+ Supported formats: JPG, PNG, WEBP, GIF
190
+ </p>
191
+ </>
192
+ )}
193
+ </div>
194
+ )}
195
+ </div>
196
+
197
+ {/* Footer */}
198
+ <div className="p-6 pt-0 flex gap-3 justify-end">
199
+ <button
200
+ onClick={onClose}
201
+ className="px-4 py-2 text-gray-300 hover:text-white transition-colors"
202
+ >
203
+ Cancel
204
+ </button>
205
+ <button
206
+ onClick={onClose}
207
+ className="px-4 py-2 bg-gradient-to-r from-blue-500 to-purple-500 text-white rounded-lg font-medium hover:from-blue-600 hover:to-purple-600 transition-all"
208
+ >
209
+ Apply
210
+ </button>
211
+ </div>
212
+ </motion.div>
213
+ </>
214
+ )}
215
+ </AnimatePresence>
216
+ )
217
+ }
app/components/Calendar.tsx ADDED
@@ -0,0 +1,301 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ 'use client'
2
+
3
+ import React, { useState } from 'react'
4
+ import { X, Minus, Square, CaretLeft, CaretRight } from '@phosphor-icons/react'
5
+ import { motion } from 'framer-motion'
6
+ import { useKV } from '../hooks/useKV'
7
+
8
+ interface CalendarProps {
9
+ onClose: () => void
10
+ }
11
+
12
+ interface Event {
13
+ id: string
14
+ date: string
15
+ title: string
16
+ type: 'holiday' | 'custom'
17
+ }
18
+
19
+ const defaultHolidays: Event[] = [
20
+ { id: '1', date: '2025-01-01', title: 'New Year\'s Day', type: 'holiday' },
21
+ { id: '2', date: '2025-02-14', title: 'Valentine\'s Day', type: 'holiday' },
22
+ { id: '3', date: '2025-03-17', title: 'St. Patrick\'s Day', type: 'holiday' },
23
+ { id: '4', date: '2025-04-20', title: 'Easter Sunday', type: 'holiday' },
24
+ { id: '5', date: '2025-05-11', title: 'Mother\'s Day', type: 'holiday' },
25
+ { id: '6', date: '2025-06-15', title: 'Father\'s Day', type: 'holiday' },
26
+ { id: '7', date: '2025-07-04', title: 'Independence Day', type: 'holiday' },
27
+ { id: '8', date: '2025-10-31', title: 'Halloween', type: 'holiday' },
28
+ { id: '9', date: '2025-11-27', title: 'Thanksgiving', type: 'holiday' },
29
+ { id: '10', date: '2025-12-25', title: 'Christmas Day', type: 'holiday' },
30
+ { id: '11', date: '2025-12-31', title: 'New Year\'s Eve', type: 'holiday' },
31
+ ]
32
+
33
+ export function Calendar({ onClose }: CalendarProps) {
34
+ const [currentDate, setCurrentDate] = useState(new Date())
35
+ const [windowPos, setWindowPos] = useState({ x: 100, y: 100 })
36
+ const [isDragging, setIsDragging] = useState(false)
37
+ const [dragStart, setDragStart] = useState({ x: 0, y: 0 })
38
+ const [events, setEvents] = useKV<Event[]>('calendar-events', defaultHolidays)
39
+ const [selectedDay, setSelectedDay] = useState<number | null>(null)
40
+
41
+ const handleMouseDown = (e: React.MouseEvent) => {
42
+ if ((e.target as HTMLElement).closest('.window-controls')) return
43
+ setIsDragging(true)
44
+ setDragStart({ x: e.clientX - windowPos.x, y: e.clientY - windowPos.y })
45
+ }
46
+
47
+ const handleMouseMove = (e: MouseEvent) => {
48
+ if (isDragging) {
49
+ setWindowPos({
50
+ x: e.clientX - dragStart.x,
51
+ y: e.clientY - dragStart.y,
52
+ })
53
+ }
54
+ }
55
+
56
+ const handleMouseUp = () => {
57
+ setIsDragging(false)
58
+ }
59
+
60
+ React.useEffect(() => {
61
+ if (isDragging) {
62
+ window.addEventListener('mousemove', handleMouseMove)
63
+ window.addEventListener('mouseup', handleMouseUp)
64
+ return () => {
65
+ window.removeEventListener('mousemove', handleMouseMove)
66
+ window.removeEventListener('mouseup', handleMouseUp)
67
+ }
68
+ }
69
+ }, [isDragging, dragStart])
70
+
71
+ const monthNames = [
72
+ 'January', 'February', 'March', 'April', 'May', 'June',
73
+ 'July', 'August', 'September', 'October', 'November', 'December'
74
+ ]
75
+
76
+ const daysOfWeek = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']
77
+
78
+ const getDaysInMonth = (date: Date) => {
79
+ const year = date.getFullYear()
80
+ const month = date.getMonth()
81
+ const firstDay = new Date(year, month, 1)
82
+ const lastDay = new Date(year, month + 1, 0)
83
+ const daysInMonth = lastDay.getDate()
84
+ const startingDayOfWeek = firstDay.getDay()
85
+
86
+ const days: (number | null)[] = []
87
+ for (let i = 0; i < startingDayOfWeek; i++) {
88
+ days.push(null)
89
+ }
90
+ for (let i = 1; i <= daysInMonth; i++) {
91
+ days.push(i)
92
+ }
93
+ return days
94
+ }
95
+
96
+ const days = getDaysInMonth(currentDate)
97
+ const today = new Date()
98
+ const isToday = (day: number | null) => {
99
+ if (!day) return false
100
+ return (
101
+ day === today.getDate() &&
102
+ currentDate.getMonth() === today.getMonth() &&
103
+ currentDate.getFullYear() === today.getFullYear()
104
+ )
105
+ }
106
+
107
+ const getEventsForDay = (day: number | null) => {
108
+ if (!day || !events) return []
109
+ const dateStr = `${currentDate.getFullYear()}-${String(currentDate.getMonth() + 1).padStart(2, '0')}-${String(day).padStart(2, '0')}`
110
+ return events.filter(event => event.date === dateStr)
111
+ }
112
+
113
+ const hasEvent = (day: number | null) => {
114
+ return getEventsForDay(day).length > 0
115
+ }
116
+
117
+ const previousMonth = () => {
118
+ setCurrentDate(new Date(currentDate.getFullYear(), currentDate.getMonth() - 1))
119
+ setSelectedDay(null)
120
+ }
121
+
122
+ const nextMonth = () => {
123
+ setCurrentDate(new Date(currentDate.getFullYear(), currentDate.getMonth() + 1))
124
+ setSelectedDay(null)
125
+ }
126
+
127
+ const handleDayClick = (day: number | null) => {
128
+ if (day) {
129
+ setSelectedDay(day)
130
+ }
131
+ }
132
+
133
+ const selectedDayEvents = selectedDay ? getEventsForDay(selectedDay) : []
134
+
135
+ return (
136
+ <motion.div
137
+ style={{ left: windowPos.x, top: windowPos.y }}
138
+ className="fixed w-[420px] bg-white rounded-lg shadow-2xl overflow-hidden flex flex-col z-30 select-none"
139
+ >
140
+ <div
141
+ onMouseDown={handleMouseDown}
142
+ className="h-11 bg-gradient-to-b from-[#f6f5f4] to-[#edebe9] border-b border-[#d0d0d0] flex items-center justify-between px-3 cursor-move"
143
+ >
144
+ <div className="flex items-center gap-2 flex-1">
145
+ <div className="flex items-center gap-1 window-controls">
146
+ <button
147
+ onClick={(e) => {
148
+ e.stopPropagation()
149
+ onClose()
150
+ }}
151
+ className="w-5 h-5 rounded-full bg-[#E95420] hover:bg-[#d14818] flex items-center justify-center group transition-colors"
152
+ >
153
+ <X size={12} weight="bold" className="text-white opacity-0 group-hover:opacity-100 transition-opacity" />
154
+ </button>
155
+ <button className="w-5 h-5 rounded-full bg-[#ddd] hover:bg-[#ccc] flex items-center justify-center group transition-colors">
156
+ <Minus size={12} weight="bold" className="text-[#666] opacity-0 group-hover:opacity-100 transition-opacity" />
157
+ </button>
158
+ <button className="w-5 h-5 rounded-full bg-[#ddd] hover:bg-[#ccc] flex items-center justify-center group transition-colors">
159
+ <Square size={10} weight="bold" className="text-[#666] opacity-0 group-hover:opacity-100 transition-opacity" />
160
+ </button>
161
+ </div>
162
+ <span className="text-sm font-medium text-[#2c2c2c] ml-2">Calendar</span>
163
+ </div>
164
+ </div>
165
+
166
+ <div className="p-6">
167
+ <div className="flex items-center justify-between mb-4">
168
+ <button
169
+ onClick={previousMonth}
170
+ className="p-2 hover:bg-[#f0f0f0] rounded"
171
+ >
172
+ <CaretLeft size={20} weight="bold" />
173
+ </button>
174
+ <div className="text-base font-semibold text-[#2c2c2c]">
175
+ {monthNames[currentDate.getMonth()]} {currentDate.getFullYear()}
176
+ </div>
177
+ <button
178
+ onClick={nextMonth}
179
+ className="p-2 hover:bg-[#f0f0f0] rounded"
180
+ >
181
+ <CaretRight size={20} weight="bold" />
182
+ </button>
183
+ </div>
184
+
185
+ <div className="grid grid-cols-7 gap-1 mb-2">
186
+ {daysOfWeek.map((day) => (
187
+ <div
188
+ key={day}
189
+ className="text-center text-xs font-medium text-[#666] py-1"
190
+ >
191
+ {day}
192
+ </div>
193
+ ))}
194
+ </div>
195
+
196
+ <div className="grid grid-cols-7 gap-1">
197
+ {days.map((day, index) => (
198
+ day ? (
199
+ <button
200
+ key={index}
201
+ onClick={() => handleDayClick(day)}
202
+ className={`
203
+ aspect-square flex flex-col items-center justify-center text-sm rounded relative
204
+ hover:bg-[#f0f0f0]
205
+ ${isToday(day) ? 'bg-[#E95420] text-white hover:bg-[#d14818] font-bold' : 'text-[#2c2c2c]'}
206
+ ${selectedDay === day && !isToday(day) ? 'bg-[#f0f0f0] ring-2 ring-[#E95420]' : ''}
207
+ `}
208
+ >
209
+ <span>{day}</span>
210
+ {hasEvent(day) && (
211
+ <div className="absolute bottom-1 flex gap-0.5">
212
+ {getEventsForDay(day).slice(0, 3).map((event, i) => (
213
+ <div
214
+ key={i}
215
+ className={`w-1 h-1 rounded-full ${
216
+ event.type === 'holiday'
217
+ ? isToday(day) ? 'bg-white' : 'bg-purple-500'
218
+ : isToday(day) ? 'bg-white' : 'bg-blue-500'
219
+ }`}
220
+ />
221
+ ))}
222
+ </div>
223
+ )}
224
+ </button>
225
+ ) : (
226
+ <div
227
+ key={index}
228
+ className="aspect-square"
229
+ />
230
+ )
231
+ ))}
232
+ </div>
233
+
234
+ {selectedDayEvents.length > 0 && (
235
+ <div className="mt-4 pt-4 border-t border-[#e0e0e0]">
236
+ <div className="text-xs text-[#666] mb-2">
237
+ {monthNames[currentDate.getMonth()]} {selectedDay}, {currentDate.getFullYear()}
238
+ </div>
239
+ <div className="space-y-2 max-h-32 overflow-y-auto">
240
+ {selectedDayEvents.map((event) => (
241
+ <div
242
+ key={event.id}
243
+ className="flex items-start gap-2 p-2 bg-[#f9f9f9] rounded"
244
+ >
245
+ <div
246
+ className={`w-2 h-2 rounded-full mt-1.5 flex-shrink-0 ${
247
+ event.type === 'holiday' ? 'bg-purple-500' : 'bg-blue-500'
248
+ }`}
249
+ />
250
+ <div className="flex-1">
251
+ <div className="text-sm font-medium text-[#2c2c2c]">
252
+ {event.title}
253
+ </div>
254
+ <div className="text-xs text-[#666]">
255
+ {event.type === 'holiday' ? 'Holiday' : 'Event'}
256
+ </div>
257
+ </div>
258
+ </div>
259
+ ))}
260
+ </div>
261
+ </div>
262
+ )}
263
+
264
+ {!selectedDay && (
265
+ <div className="mt-4 pt-4 border-t border-[#e0e0e0]">
266
+ <div className="text-xs text-[#666] mb-2">Today</div>
267
+ <div className="text-sm font-medium text-[#2c2c2c]">
268
+ {today.toLocaleDateString('en-US', {
269
+ weekday: 'long',
270
+ year: 'numeric',
271
+ month: 'long',
272
+ day: 'numeric'
273
+ })}
274
+ </div>
275
+ {getEventsForDay(today.getDate()).length > 0 && (
276
+ <div className="mt-3 space-y-2">
277
+ {getEventsForDay(today.getDate()).map((event) => (
278
+ <div
279
+ key={event.id}
280
+ className="flex items-start gap-2 p-2 bg-[#fff3e6] rounded"
281
+ >
282
+ <div
283
+ className={`w-2 h-2 rounded-full mt-1.5 flex-shrink-0 ${
284
+ event.type === 'holiday' ? 'bg-purple-500' : 'bg-blue-500'
285
+ }`}
286
+ />
287
+ <div className="flex-1">
288
+ <div className="text-sm font-medium text-[#2c2c2c]">
289
+ {event.title}
290
+ </div>
291
+ </div>
292
+ </div>
293
+ ))}
294
+ </div>
295
+ )}
296
+ </div>
297
+ )}
298
+ </div>
299
+ </motion.div>
300
+ )
301
+ }
app/components/ClaudeIntegration.tsx ADDED
@@ -0,0 +1,240 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ 'use client'
2
+
3
+ import React, { useState } from 'react'
4
+ import { motion } from 'framer-motion'
5
+ import {
6
+ Upload,
7
+ Robot,
8
+ FileText,
9
+ CheckCircle,
10
+ Warning,
11
+ Info,
12
+ Copy,
13
+ Download
14
+ } from '@phosphor-icons/react'
15
+
16
+ interface ClaudeIntegrationProps {
17
+ file: File | null
18
+ onClose: () => void
19
+ }
20
+
21
+ export function ClaudeIntegration({ file, onClose }: ClaudeIntegrationProps) {
22
+ const [uploadMethod, setUploadMethod] = useState<'direct' | 'chunked' | 'reference'>('direct')
23
+ const [processing, setProcessing] = useState(false)
24
+ const [result, setResult] = useState<string>('')
25
+
26
+ const handleProcess = async () => {
27
+ if (!file) return
28
+
29
+ setProcessing(true)
30
+
31
+ try {
32
+ if (uploadMethod === 'direct') {
33
+ // Direct upload - best for small files < 1MB
34
+ const content = await file.text()
35
+ setResult(`File: ${file.name}\n\nContent:\n${content}`)
36
+ } else if (uploadMethod === 'chunked') {
37
+ // Chunked upload - for larger files, split into manageable chunks
38
+ const CHUNK_SIZE = 50000 // 50KB chunks
39
+ const content = await file.text()
40
+ const chunks = []
41
+
42
+ for (let i = 0; i < content.length; i += CHUNK_SIZE) {
43
+ chunks.push(content.slice(i, i + CHUNK_SIZE))
44
+ }
45
+
46
+ setResult(
47
+ `File: ${file.name}\n` +
48
+ `Total chunks: ${chunks.length}\n` +
49
+ `Chunk size: ${CHUNK_SIZE} characters\n\n` +
50
+ `Instructions for Claude:\n` +
51
+ `This file has been split into ${chunks.length} chunks.\n` +
52
+ `You can process each chunk sequentially.\n\n` +
53
+ `First chunk:\n${chunks[0].slice(0, 500)}...`
54
+ )
55
+ } else {
56
+ // Reference method - store file and provide reference
57
+ setResult(
58
+ `File Reference Created:\n` +
59
+ `Name: ${file.name}\n` +
60
+ `Size: ${formatFileSize(file.size)}\n` +
61
+ `Type: ${file.type}\n\n` +
62
+ `This file has been stored in the system.\n` +
63
+ `You can reference it by name in your conversation with Claude.\n` +
64
+ `Claude can access and analyze the file content when needed.`
65
+ )
66
+ }
67
+ } catch (error) {
68
+ console.error('Error processing file:', error)
69
+ setResult('Error processing file')
70
+ } finally {
71
+ setProcessing(false)
72
+ }
73
+ }
74
+
75
+ const formatFileSize = (bytes: number) => {
76
+ const units = ['B', 'KB', 'MB', 'GB']
77
+ let size = bytes
78
+ let unitIndex = 0
79
+
80
+ while (size >= 1024 && unitIndex < units.length - 1) {
81
+ size /= 1024
82
+ unitIndex++
83
+ }
84
+
85
+ return `${size.toFixed(2)} ${units[unitIndex]}`
86
+ }
87
+
88
+ const copyToClipboard = () => {
89
+ navigator.clipboard.writeText(result)
90
+ }
91
+
92
+ return (
93
+ <motion.div
94
+ initial={{ opacity: 0, y: 20 }}
95
+ animate={{ opacity: 1, y: 0 }}
96
+ className="fixed inset-0 bg-black/50 backdrop-blur-sm z-50 flex items-center justify-center p-4"
97
+ onClick={onClose}
98
+ >
99
+ <motion.div
100
+ initial={{ scale: 0.9 }}
101
+ animate={{ scale: 1 }}
102
+ className="bg-[#2C2C2C]/95 backdrop-blur-md rounded-xl shadow-2xl max-w-2xl w-full max-h-[80vh] overflow-hidden border border-white/10"
103
+ onClick={(e) => e.stopPropagation()}
104
+ >
105
+ {/* Header */}
106
+ <div className="p-6 border-b border-white/10">
107
+ <div className="flex items-center gap-3">
108
+ <Robot size={24} className="text-blue-400" />
109
+ <h2 className="text-xl font-semibold text-white">Claude Integration</h2>
110
+ </div>
111
+ <p className="text-sm text-gray-400 mt-2">
112
+ Optimize file upload for Claude to preserve context
113
+ </p>
114
+ </div>
115
+
116
+ {/* Content */}
117
+ <div className="p-6 space-y-4 max-h-[60vh] overflow-y-auto">
118
+ {file && (
119
+ <div className="bg-white/5 rounded-lg p-4">
120
+ <div className="flex items-center gap-3">
121
+ <FileText size={24} className="text-gray-400" />
122
+ <div>
123
+ <p className="text-white font-medium">{file.name}</p>
124
+ <p className="text-sm text-gray-400">{formatFileSize(file.size)}</p>
125
+ </div>
126
+ </div>
127
+ </div>
128
+ )}
129
+
130
+ {/* Upload Methods */}
131
+ <div className="space-y-3">
132
+ <h3 className="text-sm font-medium text-gray-300 uppercase tracking-wider">
133
+ Upload Method
134
+ </h3>
135
+
136
+ <label className="flex items-start gap-3 p-3 rounded-lg border border-white/10 hover:border-white/20 cursor-pointer transition-colors">
137
+ <input
138
+ type="radio"
139
+ value="direct"
140
+ checked={uploadMethod === 'direct'}
141
+ onChange={(e) => setUploadMethod(e.target.value as any)}
142
+ className="mt-1"
143
+ />
144
+ <div>
145
+ <p className="text-white font-medium">Direct Upload</p>
146
+ <p className="text-xs text-gray-400">
147
+ Best for small files (&lt; 1MB). Uploads entire content at once.
148
+ </p>
149
+ </div>
150
+ </label>
151
+
152
+ <label className="flex items-start gap-3 p-3 rounded-lg border border-white/10 hover:border-white/20 cursor-pointer transition-colors">
153
+ <input
154
+ type="radio"
155
+ value="chunked"
156
+ checked={uploadMethod === 'chunked'}
157
+ onChange={(e) => setUploadMethod(e.target.value as any)}
158
+ className="mt-1"
159
+ />
160
+ <div>
161
+ <p className="text-white font-medium">Chunked Upload</p>
162
+ <p className="text-xs text-gray-400">
163
+ For larger files. Splits content into manageable chunks.
164
+ </p>
165
+ </div>
166
+ </label>
167
+
168
+ <label className="flex items-start gap-3 p-3 rounded-lg border border-white/10 hover:border-white/20 cursor-pointer transition-colors">
169
+ <input
170
+ type="radio"
171
+ value="reference"
172
+ checked={uploadMethod === 'reference'}
173
+ onChange={(e) => setUploadMethod(e.target.value as any)}
174
+ className="mt-1"
175
+ />
176
+ <div>
177
+ <p className="text-white font-medium">Reference Method</p>
178
+ <p className="text-xs text-gray-400">
179
+ Store file and provide reference. Claude accesses when needed.
180
+ </p>
181
+ </div>
182
+ </label>
183
+ </div>
184
+
185
+ {/* Info Box */}
186
+ <div className="bg-blue-500/10 border border-blue-500/20 rounded-lg p-4 flex gap-3">
187
+ <Info size={20} className="text-blue-400 flex-shrink-0 mt-0.5" />
188
+ <div className="text-sm text-gray-300">
189
+ <p className="font-medium mb-1">Context Preservation Tips:</p>
190
+ <ul className="space-y-1 text-xs text-gray-400">
191
+ <li>• Use direct upload for code files and short documents</li>
192
+ <li>• Use chunked upload for large documents or datasets</li>
193
+ <li>• Use reference method for binary files or archives</li>
194
+ </ul>
195
+ </div>
196
+ </div>
197
+
198
+ {/* Result */}
199
+ {result && (
200
+ <div className="bg-green-500/10 border border-green-500/20 rounded-lg p-4">
201
+ <div className="flex items-center justify-between mb-2">
202
+ <div className="flex items-center gap-2">
203
+ <CheckCircle size={20} className="text-green-400" />
204
+ <span className="text-sm font-medium text-green-400">Ready for Claude</span>
205
+ </div>
206
+ <button
207
+ onClick={copyToClipboard}
208
+ className="text-gray-400 hover:text-white transition-colors p-1"
209
+ title="Copy to clipboard"
210
+ >
211
+ <Copy size={18} />
212
+ </button>
213
+ </div>
214
+ <pre className="text-xs text-gray-300 whitespace-pre-wrap font-mono max-h-40 overflow-y-auto">
215
+ {result}
216
+ </pre>
217
+ </div>
218
+ )}
219
+ </div>
220
+
221
+ {/* Footer */}
222
+ <div className="p-6 pt-0 flex gap-3 justify-end">
223
+ <button
224
+ onClick={onClose}
225
+ className="px-4 py-2 text-gray-300 hover:text-white transition-colors"
226
+ >
227
+ Cancel
228
+ </button>
229
+ <button
230
+ onClick={handleProcess}
231
+ disabled={!file || processing}
232
+ className="px-4 py-2 bg-gradient-to-r from-blue-500 to-purple-500 text-white rounded-lg font-medium hover:from-blue-600 hover:to-purple-600 transition-all disabled:opacity-50"
233
+ >
234
+ {processing ? 'Processing...' : 'Process File'}
235
+ </button>
236
+ </div>
237
+ </motion.div>
238
+ </motion.div>
239
+ )
240
+ }
app/components/Clock.tsx ADDED
@@ -0,0 +1,188 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ 'use client'
2
+
3
+ import React, { useState, useEffect } from 'react'
4
+ import Window from './Window'
5
+
6
+ interface ClockProps {
7
+ onClose: () => void
8
+ }
9
+
10
+ export function Clock({ onClose }: ClockProps) {
11
+ const [time, setTime] = useState(new Date())
12
+ const [viewMode, setViewMode] = useState<'analog' | 'digital' | 'world'>('analog')
13
+
14
+ useEffect(() => {
15
+ const timer = setInterval(() => {
16
+ setTime(new Date())
17
+ }, 1000)
18
+
19
+ return () => clearInterval(timer)
20
+ }, [])
21
+
22
+ // Calculate rotation angles for clock hands
23
+ const seconds = time.getSeconds()
24
+ const minutes = time.getMinutes()
25
+ const hours = time.getHours()
26
+
27
+ const secondDegrees = (seconds / 60) * 360 - 90
28
+ const minuteDegrees = ((minutes / 60) * 360) + ((seconds / 60) * 6) - 90
29
+ const hourDegrees = ((hours % 12 / 12) * 360) + ((minutes / 60) * 30) - 90
30
+
31
+ const worldClocks = [
32
+ { city: 'New York', offset: -5 },
33
+ { city: 'London', offset: 0 },
34
+ { city: 'Tokyo', offset: 9 },
35
+ { city: 'Sydney', offset: 11 }
36
+ ]
37
+
38
+ const getWorldTime = (offset: number) => {
39
+ const utc = time.getTime() + (time.getTimezoneOffset() * 60000)
40
+ const cityTime = new Date(utc + (3600000 * offset))
41
+ return cityTime.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit' })
42
+ }
43
+
44
+ return (
45
+ <Window
46
+ id="clock"
47
+ title="Clock"
48
+ isOpen={true}
49
+ onClose={onClose}
50
+ width={380}
51
+ height={480}
52
+ x={window.innerWidth / 2 - 190}
53
+ y={window.innerHeight / 2 - 240}
54
+ darkMode={true}
55
+ className="clock-app-window"
56
+ >
57
+ <div className="flex flex-col h-full bg-gradient-to-b from-gray-900 via-gray-800 to-black">
58
+ {/* Tab Bar */}
59
+ <div className="flex border-b border-gray-700 bg-gray-900/50">
60
+ <button
61
+ onClick={() => setViewMode('analog')}
62
+ className={`flex-1 py-3 text-sm font-medium transition-all ${
63
+ viewMode === 'analog'
64
+ ? 'text-orange-400 border-b-2 border-orange-400'
65
+ : 'text-gray-400 hover:text-white'
66
+ }`}
67
+ >
68
+ Analog
69
+ </button>
70
+ <button
71
+ onClick={() => setViewMode('digital')}
72
+ className={`flex-1 py-3 text-sm font-medium transition-all ${
73
+ viewMode === 'digital'
74
+ ? 'text-orange-400 border-b-2 border-orange-400'
75
+ : 'text-gray-400 hover:text-white'
76
+ }`}
77
+ >
78
+ Digital
79
+ </button>
80
+ <button
81
+ onClick={() => setViewMode('world')}
82
+ className={`flex-1 py-3 text-sm font-medium transition-all ${
83
+ viewMode === 'world'
84
+ ? 'text-orange-400 border-b-2 border-orange-400'
85
+ : 'text-gray-400 hover:text-white'
86
+ }`}
87
+ >
88
+ World Clock
89
+ </button>
90
+ </div>
91
+
92
+ {/* Clock Display */}
93
+ <div className="flex-1 flex items-center justify-center p-6">
94
+ {viewMode === 'analog' && (
95
+ <div className="flex flex-col items-center">
96
+ <div className="relative w-64 h-64">
97
+ {/* Clock Face */}
98
+ <div className="absolute inset-0 rounded-full bg-gradient-to-br from-gray-800 to-black shadow-inner border-4 border-gray-700">
99
+ {/* Hour Markers */}
100
+ {[...Array(12)].map((_, i) => (
101
+ <div key={i}>
102
+ <div
103
+ className="absolute bg-white"
104
+ style={{
105
+ width: i % 3 === 0 ? '3px' : '1px',
106
+ height: i % 3 === 0 ? '12px' : '8px',
107
+ top: '10px',
108
+ left: '50%',
109
+ transform: `translateX(-50%) rotate(${i * 30}deg)`,
110
+ transformOrigin: '50% 118px'
111
+ }}
112
+ />
113
+ </div>
114
+ ))}
115
+
116
+ {/* Clock Hands */}
117
+ <div
118
+ className="absolute top-1/2 left-1/2 w-2 h-20 bg-white rounded-full origin-bottom shadow-lg"
119
+ style={{
120
+ transform: `translate(-50%, -100%) rotate(${hourDegrees}deg)`
121
+ }}
122
+ />
123
+ <div
124
+ className="absolute top-1/2 left-1/2 w-1.5 h-28 bg-gray-300 rounded-full origin-bottom shadow-lg"
125
+ style={{
126
+ transform: `translate(-50%, -100%) rotate(${minuteDegrees}deg)`
127
+ }}
128
+ />
129
+ <div
130
+ className="absolute top-1/2 left-1/2 w-0.5 h-32 bg-orange-500 rounded-full origin-bottom"
131
+ style={{
132
+ transform: `translate(-50%, -100%) rotate(${secondDegrees}deg)`
133
+ }}
134
+ />
135
+
136
+ {/* Center Dot */}
137
+ <div className="absolute top-1/2 left-1/2 w-4 h-4 bg-orange-500 rounded-full transform -translate-x-1/2 -translate-y-1/2 shadow-lg z-10" />
138
+ </div>
139
+ </div>
140
+
141
+ {/* Digital Time Below Analog Clock */}
142
+ <div className="mt-6 text-center">
143
+ <div className="text-2xl font-mono text-gray-300">
144
+ {time.toLocaleTimeString()}
145
+ </div>
146
+ <div className="text-sm text-gray-500 mt-2">
147
+ {time.toLocaleDateString('en-US', { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' })}
148
+ </div>
149
+ </div>
150
+ </div>
151
+ )}
152
+
153
+ {viewMode === 'digital' && (
154
+ <div className="text-center">
155
+ <div className="text-7xl font-mono font-bold text-transparent bg-clip-text bg-gradient-to-r from-orange-400 to-orange-600">
156
+ {time.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', second: '2-digit' })}
157
+ </div>
158
+ <div className="mt-8 text-2xl text-gray-400">
159
+ {time.toLocaleDateString('en-US', { weekday: 'long' })}
160
+ </div>
161
+ <div className="text-lg text-gray-500">
162
+ {time.toLocaleDateString('en-US', { year: 'numeric', month: 'long', day: 'numeric' })}
163
+ </div>
164
+ </div>
165
+ )}
166
+
167
+ {viewMode === 'world' && (
168
+ <div className="w-full max-w-sm">
169
+ <div className="space-y-4">
170
+ {worldClocks.map((clock) => (
171
+ <div key={clock.city} className="bg-gray-800 rounded-lg p-4 flex justify-between items-center">
172
+ <div>
173
+ <div className="text-white font-medium">{clock.city}</div>
174
+ <div className="text-xs text-gray-400">UTC{clock.offset >= 0 ? '+' : ''}{clock.offset}</div>
175
+ </div>
176
+ <div className="text-2xl font-mono text-orange-400">
177
+ {getWorldTime(clock.offset)}
178
+ </div>
179
+ </div>
180
+ ))}
181
+ </div>
182
+ </div>
183
+ )}
184
+ </div>
185
+ </div>
186
+ </Window>
187
+ )
188
+ }
app/components/CodeExecutor.tsx ADDED
@@ -0,0 +1,306 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ 'use client'
2
+
3
+ import React, { useState } from 'react'
4
+ import Window from './Window'
5
+ import Editor from '@monaco-editor/react'
6
+ import {
7
+ Play,
8
+ Code,
9
+ FileText,
10
+ Download,
11
+ Copy,
12
+ CheckCircle,
13
+ Warning,
14
+ CloudArrowUp
15
+ } from '@phosphor-icons/react'
16
+
17
+ interface CodeExecutorProps {
18
+ onClose: () => void
19
+ initialCode?: string
20
+ language?: string
21
+ }
22
+
23
+ export function CodeExecutor({ onClose, initialCode = '', language = 'python' }: CodeExecutorProps) {
24
+ const [code, setCode] = useState(initialCode || `# Reuben OS Code Executor
25
+ # Write your Python code here and click Run to execute
26
+
27
+ import matplotlib.pyplot as plt
28
+ import numpy as np
29
+
30
+ # Create sample data
31
+ x = np.linspace(0, 10, 100)
32
+ y = np.sin(x)
33
+
34
+ # Create plot
35
+ plt.figure(figsize=(10, 6))
36
+ plt.plot(x, y, 'b-', linewidth=2, label='sin(x)')
37
+ plt.grid(True, alpha=0.3)
38
+ plt.xlabel('X axis')
39
+ plt.ylabel('Y axis')
40
+ plt.title('Sample Plot in Reuben OS')
41
+ plt.legend()
42
+
43
+ # This will automatically save and display the plot
44
+ plt.show()
45
+
46
+ print("Plot generated successfully!")
47
+ print(f"X range: {x[0]:.2f} to {x[-1]:.2f}")
48
+ print(f"Y range: {y.min():.2f} to {y.max():.2f}")`)
49
+
50
+ const [output, setOutput] = useState('')
51
+ const [isRunning, setIsRunning] = useState(false)
52
+ const [error, setError] = useState('')
53
+ const [plotPath, setPlotPath] = useState('')
54
+ const [selectedLanguage, setSelectedLanguage] = useState(language)
55
+
56
+ const executeCode = async () => {
57
+ setIsRunning(true)
58
+ setOutput('')
59
+ setError('')
60
+ setPlotPath('')
61
+
62
+ try {
63
+ // Save code to file system first
64
+ const sessionId = `exec_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`
65
+ const saveResponse = await fetch('/api/code/save', {
66
+ method: 'POST',
67
+ headers: { 'Content-Type': 'application/json' },
68
+ body: JSON.stringify({
69
+ sessionId,
70
+ code: [{
71
+ name: `script.${selectedLanguage === 'python' ? 'py' : selectedLanguage === 'javascript' ? 'js' : 'html'}`,
72
+ language: selectedLanguage,
73
+ content: code
74
+ }],
75
+ timestamp: Date.now()
76
+ })
77
+ })
78
+
79
+ if (!saveResponse.ok) {
80
+ throw new Error('Failed to save code')
81
+ }
82
+
83
+ // Execute based on language
84
+ if (selectedLanguage === 'python') {
85
+ // For Python, we need MCP to execute it
86
+ setOutput('Python code saved! Use MCP tools to execute:\n')
87
+ setOutput(prev => prev + `\nSession ID: ${sessionId}\n`)
88
+ setOutput(prev => prev + `\nFile saved to: data/vscode_sessions/${sessionId}/script.py\n`)
89
+ setOutput(prev => prev + '\n📝 Note: To execute Python code, use the MCP execute_python_code tool from Claude Desktop.')
90
+
91
+ // Show sample MCP command
92
+ setOutput(prev => prev + '\n\n💡 MCP Command Example:\n')
93
+ setOutput(prev => prev + 'execute_python_code(code=<your_code>, save_output=true)\n')
94
+
95
+ // If it's matplotlib code, suggest using execute_matplotlib_code
96
+ if (code.includes('matplotlib') || code.includes('plt.')) {
97
+ setOutput(prev => prev + '\n🎨 For matplotlib plots, use:\n')
98
+ setOutput(prev => prev + 'execute_matplotlib_code(code=<your_code>, output_format="png")')
99
+ }
100
+ } else if (selectedLanguage === 'html' || selectedLanguage === 'javascript') {
101
+ // For HTML/JS, we can execute in browser
102
+ executeWebCode()
103
+ }
104
+ } catch (err) {
105
+ setError(err instanceof Error ? err.message : 'Unknown error occurred')
106
+ } finally {
107
+ setIsRunning(false)
108
+ }
109
+ }
110
+
111
+ const executeWebCode = () => {
112
+ if (selectedLanguage === 'html') {
113
+ // Create iframe for HTML preview
114
+ const iframe = document.createElement('iframe')
115
+ iframe.style.width = '100%'
116
+ iframe.style.height = '100%'
117
+ iframe.style.border = 'none'
118
+ iframe.srcdoc = code
119
+
120
+ const outputElement = document.getElementById('code-output')
121
+ if (outputElement) {
122
+ outputElement.innerHTML = ''
123
+ outputElement.appendChild(iframe)
124
+ }
125
+ setOutput('HTML rendered in preview pane')
126
+ } else if (selectedLanguage === 'javascript') {
127
+ // Execute JavaScript in sandboxed environment
128
+ try {
129
+ // Capture console.log outputs
130
+ const logs: string[] = []
131
+ const originalLog = console.log
132
+ console.log = (...args) => {
133
+ logs.push(args.map(arg => String(arg)).join(' '))
134
+ }
135
+
136
+ // Execute the code
137
+ const func = new Function(code)
138
+ const result = func()
139
+
140
+ // Restore console.log
141
+ console.log = originalLog
142
+
143
+ // Display output
144
+ let outputText = logs.join('\n')
145
+ if (result !== undefined) {
146
+ outputText += `\n\nReturn value: ${JSON.stringify(result, null, 2)}`
147
+ }
148
+ setOutput(outputText || 'Code executed successfully (no output)')
149
+ } catch (err) {
150
+ setError(err instanceof Error ? err.message : 'JavaScript execution error')
151
+ }
152
+ }
153
+ }
154
+
155
+ const copyOutput = () => {
156
+ navigator.clipboard.writeText(output || error)
157
+
158
+ // Show copied feedback
159
+ const copiedDiv = document.createElement('div')
160
+ 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'
161
+ copiedDiv.innerHTML = '<svg class="w-4 h-4" fill="currentColor" viewBox="0 0 20 20"><path d="M9 2a1 1 0 000 2h2a1 1 0 100-2H9z"></path><path fill-rule="evenodd" d="M4 5a2 2 0 012-2 1 1 0 000 2H6a2 2 0 100 4h8a2 2 0 100-4 1 1 0 100-2 2 2 0 00-2 2H8a2 2 0 00-2-2z" clip-rule="evenodd"></path></svg> Copied!'
162
+ document.body.appendChild(copiedDiv)
163
+
164
+ setTimeout(() => {
165
+ copiedDiv.remove()
166
+ }, 2000)
167
+ }
168
+
169
+ return (
170
+ <Window
171
+ id="code-executor"
172
+ title="Code Executor - Reuben OS"
173
+ isOpen={true}
174
+ onClose={onClose}
175
+ width={1200}
176
+ height={700}
177
+ x={100}
178
+ y={50}
179
+ darkMode={true}
180
+ >
181
+ <div className="flex flex-col h-full bg-[#1e1e1e]">
182
+ {/* Header */}
183
+ <div className="flex items-center justify-between bg-[#2d2d2d] px-4 py-2 border-b border-[#3e3e3e]">
184
+ <div className="flex items-center gap-4">
185
+ <Code size={20} weight="bold" className="text-blue-400" />
186
+ <select
187
+ value={selectedLanguage}
188
+ onChange={(e) => setSelectedLanguage(e.target.value)}
189
+ className="bg-[#1e1e1e] text-white px-3 py-1 rounded border border-[#3e3e3e] text-sm"
190
+ >
191
+ <option value="python">Python</option>
192
+ <option value="javascript">JavaScript</option>
193
+ <option value="html">HTML</option>
194
+ </select>
195
+ </div>
196
+
197
+ <div className="flex items-center gap-2">
198
+ <button
199
+ onClick={executeCode}
200
+ disabled={isRunning}
201
+ className="px-4 py-1 bg-green-600 hover:bg-green-700 disabled:bg-gray-600 text-white rounded text-sm flex items-center gap-2"
202
+ >
203
+ <Play size={16} weight="fill" />
204
+ {isRunning ? 'Running...' : 'Run'}
205
+ </button>
206
+
207
+ <button
208
+ onClick={() => setCode('')}
209
+ className="px-3 py-1 bg-gray-600 hover:bg-gray-700 text-white rounded text-sm"
210
+ >
211
+ Clear
212
+ </button>
213
+ </div>
214
+ </div>
215
+
216
+ {/* Main Content */}
217
+ <div className="flex flex-1 overflow-hidden">
218
+ {/* Code Editor */}
219
+ <div className="w-1/2 border-r border-[#3e3e3e]">
220
+ <Editor
221
+ theme="vs-dark"
222
+ language={selectedLanguage}
223
+ value={code}
224
+ onChange={(value) => setCode(value || '')}
225
+ options={{
226
+ minimap: { enabled: false },
227
+ fontSize: 14,
228
+ lineNumbers: 'on',
229
+ roundedSelection: false,
230
+ scrollBeyondLastLine: false,
231
+ automaticLayout: true,
232
+ wordWrap: 'on'
233
+ }}
234
+ />
235
+ </div>
236
+
237
+ {/* Output Panel */}
238
+ <div className="w-1/2 flex flex-col bg-[#1e1e1e]">
239
+ <div className="flex items-center justify-between bg-[#252526] px-4 py-2 border-b border-[#3e3e3e]">
240
+ <span className="text-sm text-gray-300 flex items-center gap-2">
241
+ <FileText size={16} />
242
+ Output
243
+ </span>
244
+ <button
245
+ onClick={copyOutput}
246
+ className="text-gray-400 hover:text-white transition-colors"
247
+ disabled={!output && !error}
248
+ >
249
+ <Copy size={16} />
250
+ </button>
251
+ </div>
252
+
253
+ <div id="code-output" className="flex-1 p-4 font-mono text-sm overflow-auto">
254
+ {error ? (
255
+ <div className="text-red-400">
256
+ <div className="flex items-center gap-2 mb-2">
257
+ <Warning size={16} />
258
+ Error:
259
+ </div>
260
+ <pre className="whitespace-pre-wrap">{error}</pre>
261
+ </div>
262
+ ) : output ? (
263
+ <div className="text-green-400">
264
+ <pre className="whitespace-pre-wrap">{output}</pre>
265
+ {plotPath && (
266
+ <div className="mt-4">
267
+ <p className="text-blue-400 mb-2">Plot saved to:</p>
268
+ <code className="bg-[#2d2d2d] px-2 py-1 rounded">{plotPath}</code>
269
+ </div>
270
+ )}
271
+ </div>
272
+ ) : (
273
+ <div className="text-gray-500 flex items-center justify-center h-full">
274
+ <div className="text-center">
275
+ <Code size={48} className="mx-auto mb-4 opacity-20" />
276
+ <p>Write code and click Run to see output</p>
277
+ <p className="text-xs mt-2 text-gray-600">
278
+ Python execution requires MCP tools from Claude Desktop
279
+ </p>
280
+ </div>
281
+ </div>
282
+ )}
283
+ </div>
284
+
285
+ {/* Info Panel */}
286
+ <div className="bg-[#252526] p-3 border-t border-[#3e3e3e]">
287
+ <div className="text-xs text-gray-400">
288
+ {selectedLanguage === 'python' ? (
289
+ <div className="flex items-center gap-2">
290
+ <CloudArrowUp size={14} />
291
+ <span>Python code is saved to disk. Use MCP tools to execute.</span>
292
+ </div>
293
+ ) : (
294
+ <div className="flex items-center gap-2">
295
+ <CheckCircle size={14} />
296
+ <span>HTML/JS executes directly in browser sandbox.</span>
297
+ </div>
298
+ )}
299
+ </div>
300
+ </div>
301
+ </div>
302
+ </div>
303
+ </div>
304
+ </Window>
305
+ )
306
+ }
app/components/CodePlayground.tsx ADDED
@@ -0,0 +1,668 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ 'use client'
2
+
3
+ import React, { useState, useRef, useEffect } from 'react'
4
+ import Window from './Window'
5
+ import Editor from '@monaco-editor/react'
6
+ import {
7
+ Code,
8
+ Play,
9
+ FileHtml,
10
+ FileCss,
11
+ FileJs,
12
+ FilePy,
13
+ FileCode,
14
+ Download,
15
+ Upload,
16
+ FloppyDisk,
17
+ Eye,
18
+ EyeSlash,
19
+ ArrowsOutSimple,
20
+ ArrowsInSimple,
21
+ Plus,
22
+ X,
23
+ Globe,
24
+ Lock,
25
+ Users,
26
+ Folder,
27
+ Terminal as TerminalIcon,
28
+ Lightning
29
+ } from '@phosphor-icons/react'
30
+
31
+ interface CodePlaygroundProps {
32
+ onClose: () => void
33
+ userSession: string
34
+ }
35
+
36
+ interface Tab {
37
+ id: string
38
+ name: string
39
+ language: string
40
+ content: string
41
+ isPublic?: boolean
42
+ type: 'html' | 'css' | 'javascript' | 'python' | 'react' | 'flutter' | 'typescript'
43
+ }
44
+
45
+ interface ExecutionResult {
46
+ output: string
47
+ error?: string
48
+ timestamp: number
49
+ }
50
+
51
+ export function CodePlayground({ onClose, userSession }: CodePlaygroundProps) {
52
+ const [activeTab, setActiveTab] = useState<string>('main')
53
+ const [showPreview, setShowPreview] = useState(true)
54
+ const [showConsole, setShowConsole] = useState(true)
55
+ const [isFullscreen, setIsFullscreen] = useState(false)
56
+ const [isSaving, setIsSaving] = useState(false)
57
+ const [isExecuting, setIsExecuting] = useState(false)
58
+ const [executionResults, setExecutionResults] = useState<ExecutionResult[]>([])
59
+ const [publicFiles, setPublicFiles] = useState<Tab[]>([])
60
+ const previewRef = useRef<HTMLIFrameElement>(null)
61
+ const [draggedTab, setDraggedTab] = useState<string | null>(null)
62
+ const [dragOverTab, setDragOverTab] = useState<string | null>(null)
63
+
64
+ // Enhanced tab icons with more languages
65
+ const getTabIcon = (type: string) => {
66
+ const icons: Record<string, JSX.Element> = {
67
+ 'html': <FileHtml size={16} weight="fill" className="text-orange-500" />,
68
+ 'css': <FileCss size={16} weight="fill" className="text-blue-500" />,
69
+ 'javascript': <FileJs size={16} weight="fill" className="text-yellow-500" />,
70
+ 'typescript': <FileCode size={16} weight="fill" className="text-blue-600" />,
71
+ 'python': <FilePy size={16} weight="fill" className="text-green-500" />,
72
+ 'react': <FileJs size={16} weight="fill" className="text-cyan-500" />,
73
+ 'flutter': <FileCode size={16} weight="fill" className="text-blue-400" />
74
+ }
75
+ return icons[type] || <Code size={16} weight="fill" className="text-gray-500" />
76
+ }
77
+
78
+ const [tabs, setTabs] = useState<Tab[]>([
79
+ {
80
+ id: 'main',
81
+ name: 'main.py',
82
+ language: 'python',
83
+ type: 'python',
84
+ content: `# Welcome to WebOS Code Playground!
85
+ # You can write and execute Python code here
86
+
87
+ def greet(name):
88
+ return f"Hello, {name}! Welcome to WebOS"
89
+
90
+ # Example usage
91
+ result = greet("Developer")
92
+ print(result)
93
+
94
+ # Your code here
95
+ for i in range(5):
96
+ print(f"Count: {i}")
97
+ `,
98
+ isPublic: false
99
+ }
100
+ ])
101
+
102
+ // Load public files from backend
103
+ useEffect(() => {
104
+ loadPublicFiles()
105
+ }, [])
106
+
107
+ const loadPublicFiles = async () => {
108
+ try {
109
+ const response = await fetch('/api/code/public')
110
+ if (response.ok) {
111
+ const files = await response.json()
112
+ setPublicFiles(files)
113
+ }
114
+ } catch (error) {
115
+ console.error('Failed to load public files:', error)
116
+ }
117
+ }
118
+
119
+ // Tab drag and drop handlers
120
+ const handleDragStart = (e: React.DragEvent, tabId: string) => {
121
+ setDraggedTab(tabId)
122
+ e.dataTransfer.effectAllowed = 'move'
123
+ }
124
+
125
+ const handleDragOver = (e: React.DragEvent, tabId: string) => {
126
+ e.preventDefault()
127
+ if (draggedTab && draggedTab !== tabId) {
128
+ setDragOverTab(tabId)
129
+ }
130
+ }
131
+
132
+ const handleDragLeave = () => {
133
+ setDragOverTab(null)
134
+ }
135
+
136
+ const handleDrop = (e: React.DragEvent, targetTabId: string) => {
137
+ e.preventDefault()
138
+
139
+ if (draggedTab && draggedTab !== targetTabId) {
140
+ const draggedIndex = tabs.findIndex(t => t.id === draggedTab)
141
+ const targetIndex = tabs.findIndex(t => t.id === targetTabId)
142
+
143
+ if (draggedIndex !== -1 && targetIndex !== -1) {
144
+ const newTabs = [...tabs]
145
+ const [draggedTabObj] = newTabs.splice(draggedIndex, 1)
146
+ newTabs.splice(targetIndex, 0, draggedTabObj)
147
+ setTabs(newTabs)
148
+ }
149
+ }
150
+
151
+ setDraggedTab(null)
152
+ setDragOverTab(null)
153
+ }
154
+
155
+ const handleDragEnd = () => {
156
+ setDraggedTab(null)
157
+ setDragOverTab(null)
158
+ }
159
+
160
+ // Create new tab with language selection
161
+ const createNewTab = (language: Tab['type']) => {
162
+ const templates: Record<Tab['type'], { name: string, content: string, lang: string }> = {
163
+ 'python': {
164
+ name: 'script.py',
165
+ lang: 'python',
166
+ content: `# Python Script
167
+ def main():
168
+ print("Hello from Python!")
169
+
170
+ if __name__ == "__main__":
171
+ main()
172
+ `
173
+ },
174
+ 'javascript': {
175
+ name: 'script.js',
176
+ lang: 'javascript',
177
+ content: `// JavaScript Code
178
+ console.log("Hello from JavaScript!");
179
+
180
+ function calculate(a, b) {
181
+ return a + b;
182
+ }
183
+
184
+ console.log("Result:", calculate(5, 3));
185
+ `
186
+ },
187
+ 'typescript': {
188
+ name: 'script.ts',
189
+ lang: 'typescript',
190
+ content: `// TypeScript Code
191
+ interface User {
192
+ name: string;
193
+ age: number;
194
+ }
195
+
196
+ function greetUser(user: User): void {
197
+ console.log(\`Hello, \${user.name}! You are \${user.age} years old.\`);
198
+ }
199
+
200
+ greetUser({ name: "Alice", age: 25 });
201
+ `
202
+ },
203
+ 'react': {
204
+ name: 'App.jsx',
205
+ lang: 'javascript',
206
+ content: `// React Component
207
+ import React, { useState } from 'react';
208
+
209
+ function App() {
210
+ const [count, setCount] = useState(0);
211
+
212
+ return (
213
+ <div>
214
+ <h1>React Counter</h1>
215
+ <p>Count: {count}</p>
216
+ <button onClick={() => setCount(count + 1)}>
217
+ Increment
218
+ </button>
219
+ </div>
220
+ );
221
+ }
222
+
223
+ export default App;
224
+ `
225
+ },
226
+ 'flutter': {
227
+ name: 'main.dart',
228
+ lang: 'dart',
229
+ content: `// Flutter Widget
230
+ import 'package:flutter/material.dart';
231
+
232
+ class MyWidget extends StatefulWidget {
233
+ @override
234
+ _MyWidgetState createState() => _MyWidgetState();
235
+ }
236
+
237
+ class _MyWidgetState extends State<MyWidget> {
238
+ int counter = 0;
239
+
240
+ @override
241
+ Widget build(BuildContext context) {
242
+ return Scaffold(
243
+ appBar: AppBar(title: Text('Flutter App')),
244
+ body: Center(
245
+ child: Column(
246
+ mainAxisAlignment: MainAxisAlignment.center,
247
+ children: [
248
+ Text('Counter: $counter'),
249
+ ElevatedButton(
250
+ onPressed: () => setState(() => counter++),
251
+ child: Text('Increment'),
252
+ ),
253
+ ],
254
+ ),
255
+ ),
256
+ );
257
+ }
258
+ }
259
+ `
260
+ },
261
+ 'html': {
262
+ name: 'index.html',
263
+ lang: 'html',
264
+ content: `<!DOCTYPE html>
265
+ <html>
266
+ <head>
267
+ <title>HTML Page</title>
268
+ </head>
269
+ <body>
270
+ <h1>Hello WebOS!</h1>
271
+ </body>
272
+ </html>`
273
+ },
274
+ 'css': {
275
+ name: 'style.css',
276
+ lang: 'css',
277
+ content: `/* CSS Styles */
278
+ body {
279
+ font-family: Arial, sans-serif;
280
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
281
+ }
282
+ `
283
+ }
284
+ }
285
+
286
+ const template = templates[language]
287
+ const newTab: Tab = {
288
+ id: `tab_${Date.now()}`,
289
+ name: template.name,
290
+ language: template.lang,
291
+ type: language,
292
+ content: template.content,
293
+ isPublic: false
294
+ }
295
+
296
+ setTabs(prev => [...prev, newTab])
297
+ setActiveTab(newTab.id)
298
+ }
299
+
300
+ // Execute code based on language
301
+ const executeCode = async () => {
302
+ setIsExecuting(true)
303
+ const activeTabData = tabs.find(t => t.id === activeTab)
304
+
305
+ if (!activeTabData) {
306
+ setIsExecuting(false)
307
+ return
308
+ }
309
+
310
+ try {
311
+ const response = await fetch('/api/code/execute', {
312
+ method: 'POST',
313
+ headers: { 'Content-Type': 'application/json' },
314
+ body: JSON.stringify({
315
+ sessionId: userSession,
316
+ language: activeTabData.type,
317
+ code: activeTabData.content,
318
+ timestamp: Date.now()
319
+ })
320
+ })
321
+
322
+ const result = await response.json()
323
+
324
+ setExecutionResults(prev => [...prev, {
325
+ output: result.output || 'No output',
326
+ error: result.error,
327
+ timestamp: Date.now()
328
+ }])
329
+ } catch (error) {
330
+ setExecutionResults(prev => [...prev, {
331
+ output: '',
332
+ error: `Execution failed: ${error}`,
333
+ timestamp: Date.now()
334
+ }])
335
+ }
336
+
337
+ setIsExecuting(false)
338
+ }
339
+
340
+ // Save to public folder
341
+ const saveToPublic = async () => {
342
+ const activeTabData = tabs.find(t => t.id === activeTab)
343
+ if (!activeTabData) return
344
+
345
+ setIsSaving(true)
346
+ try {
347
+ const response = await fetch('/api/code/public/save', {
348
+ method: 'POST',
349
+ headers: { 'Content-Type': 'application/json' },
350
+ body: JSON.stringify({
351
+ sessionId: userSession,
352
+ file: {
353
+ ...activeTabData,
354
+ isPublic: true,
355
+ author: userSession
356
+ }
357
+ })
358
+ })
359
+
360
+ if (response.ok) {
361
+ setTabs(prev => prev.map(tab =>
362
+ tab.id === activeTab ? { ...tab, isPublic: true } : tab
363
+ ))
364
+ await loadPublicFiles()
365
+ }
366
+ } catch (error) {
367
+ console.error('Failed to save to public:', error)
368
+ }
369
+ setIsSaving(false)
370
+ }
371
+
372
+ const handleEditorChange = (value: string | undefined) => {
373
+ if (!value) return
374
+
375
+ setTabs(prev => prev.map(tab =>
376
+ tab.id === activeTab ? { ...tab, content: value } : tab
377
+ ))
378
+ }
379
+
380
+ const activeTabContent = tabs.find(t => t.id === activeTab)
381
+
382
+ // Close tab
383
+ const closeTab = (tabId: string) => {
384
+ if (tabs.length === 1) return // Keep at least one tab
385
+
386
+ setTabs(prev => prev.filter(t => t.id !== tabId))
387
+ if (activeTab === tabId) {
388
+ setActiveTab(tabs[0].id)
389
+ }
390
+ }
391
+
392
+ // Load file from public
393
+ const loadPublicFile = (file: Tab) => {
394
+ const newTab: Tab = {
395
+ ...file,
396
+ id: `tab_${Date.now()}`,
397
+ isPublic: false
398
+ }
399
+ setTabs(prev => [...prev, newTab])
400
+ setActiveTab(newTab.id)
401
+ }
402
+
403
+ return (
404
+ <Window
405
+ id="code-playground"
406
+ title={`Code Playground - Session: ${userSession.substring(0, 8)}...`}
407
+ isOpen={true}
408
+ onClose={onClose}
409
+ width={isFullscreen ? window.innerWidth : 1400}
410
+ height={isFullscreen ? window.innerHeight - 32 : 850}
411
+ x={isFullscreen ? 0 : 50}
412
+ y={isFullscreen ? 32 : 30}
413
+ darkMode={true}
414
+ className="code-playground-window"
415
+ >
416
+ <div className="flex flex-col h-full bg-[#1e1e1e]">
417
+ {/* Toolbar */}
418
+ <div className="flex items-center justify-between bg-[#2d2d2d] px-4 py-2 border-b border-[#3e3e3e]">
419
+ <div className="flex items-center gap-3">
420
+ <Lightning size={20} weight="bold" className="text-yellow-400" />
421
+ <span className="text-gray-300 text-sm font-medium">Multi-Language Playground</span>
422
+ <div className="flex items-center gap-1 px-2 py-1 bg-[#1e1e1e] rounded text-xs text-gray-400">
423
+ <Users size={14} />
424
+ <span>{userSession.substring(0, 8)}</span>
425
+ </div>
426
+ </div>
427
+
428
+ <div className="flex items-center gap-2">
429
+ <button
430
+ onClick={executeCode}
431
+ disabled={isExecuting}
432
+ className="px-3 py-1 bg-green-600 hover:bg-green-700 text-white rounded text-sm flex items-center gap-2 disabled:opacity-50"
433
+ >
434
+ <Play size={16} weight="fill" />
435
+ {isExecuting ? 'Running...' : 'Run'}
436
+ </button>
437
+
438
+ <button
439
+ onClick={saveToPublic}
440
+ disabled={isSaving}
441
+ className="px-3 py-1 bg-blue-600 hover:bg-blue-700 text-white rounded text-sm flex items-center gap-2 disabled:opacity-50"
442
+ >
443
+ <Globe size={16} />
444
+ {isSaving ? 'Publishing...' : 'Publish'}
445
+ </button>
446
+
447
+ <button
448
+ onClick={() => setShowConsole(!showConsole)}
449
+ className="px-3 py-1 bg-gray-600 hover:bg-gray-700 text-white rounded text-sm flex items-center gap-2"
450
+ >
451
+ <TerminalIcon size={16} />
452
+ Console
453
+ </button>
454
+
455
+ <button
456
+ onClick={() => setShowPreview(!showPreview)}
457
+ className="px-3 py-1 bg-gray-600 hover:bg-gray-700 text-white rounded text-sm flex items-center gap-2"
458
+ >
459
+ {showPreview ? <EyeSlash size={16} /> : <Eye size={16} />}
460
+ Preview
461
+ </button>
462
+
463
+ <button
464
+ onClick={() => setIsFullscreen(!isFullscreen)}
465
+ className="px-3 py-1 bg-gray-600 hover:bg-gray-700 text-white rounded text-sm"
466
+ >
467
+ {isFullscreen ? <ArrowsInSimple size={16} /> : <ArrowsOutSimple size={16} />}
468
+ </button>
469
+ </div>
470
+ </div>
471
+
472
+ {/* Main Content Area */}
473
+ <div className="flex flex-1 overflow-hidden">
474
+ {/* Sidebar - Public Files */}
475
+ <div className="w-64 bg-[#252526] border-r border-[#3e3e3e] flex flex-col">
476
+ <div className="p-3 border-b border-[#3e3e3e]">
477
+ <div className="flex items-center gap-2 text-gray-300 text-sm font-medium">
478
+ <Folder size={16} />
479
+ Public Files
480
+ </div>
481
+ </div>
482
+ <div className="flex-1 overflow-y-auto p-2">
483
+ {publicFiles.map(file => (
484
+ <button
485
+ key={file.id}
486
+ onClick={() => loadPublicFile(file)}
487
+ className="w-full text-left px-2 py-1.5 text-xs text-gray-400 hover:bg-[#2d2d2d] rounded flex items-center gap-2"
488
+ >
489
+ {getTabIcon(file.type)}
490
+ <span className="truncate">{file.name}</span>
491
+ </button>
492
+ ))}
493
+ </div>
494
+ </div>
495
+
496
+ {/* Editor Section */}
497
+ <div className={`flex flex-col ${showPreview ? 'w-1/2' : 'flex-1'}`}>
498
+ {/* Tabs with drag and drop */}
499
+ <div className="flex bg-[#252526] border-b border-[#3e3e3e] items-center">
500
+ <div className="flex-1 flex overflow-x-auto">
501
+ {tabs.map(tab => (
502
+ <div
503
+ key={tab.id}
504
+ draggable
505
+ onDragStart={(e) => handleDragStart(e, tab.id)}
506
+ onDragOver={(e) => handleDragOver(e, tab.id)}
507
+ onDragLeave={handleDragLeave}
508
+ onDrop={(e) => handleDrop(e, tab.id)}
509
+ onDragEnd={handleDragEnd}
510
+ className={`
511
+ flex items-center gap-2 px-3 py-2 text-sm border-r border-[#3e3e3e]
512
+ cursor-move transition-all select-none
513
+ ${activeTab === tab.id ? 'bg-[#1e1e1e] text-white' : 'text-gray-400 hover:text-white hover:bg-[#2d2d2d]'}
514
+ ${dragOverTab === tab.id ? 'border-l-2 border-l-blue-500' : ''}
515
+ `}
516
+ onClick={() => setActiveTab(tab.id)}
517
+ >
518
+ {getTabIcon(tab.type)}
519
+ <span>{tab.name}</span>
520
+ {tab.isPublic && (
521
+ <Globe size={12} className="text-green-400" />
522
+ )}
523
+ {tabs.length > 1 && (
524
+ <button
525
+ onClick={(e) => {
526
+ e.stopPropagation()
527
+ closeTab(tab.id)
528
+ }}
529
+ className="ml-1 hover:bg-[#3e3e3e] rounded p-0.5"
530
+ >
531
+ <X size={12} />
532
+ </button>
533
+ )}
534
+ </div>
535
+ ))}
536
+ </div>
537
+
538
+ {/* Add new tab dropdown */}
539
+ <div className="relative group">
540
+ <button className="px-3 py-2 text-gray-400 hover:text-white hover:bg-[#2d2d2d]">
541
+ <Plus size={16} />
542
+ </button>
543
+ <div className="absolute right-0 top-full mt-1 bg-[#252526] border border-[#3e3e3e] rounded shadow-lg opacity-0 invisible group-hover:opacity-100 group-hover:visible transition-all z-10">
544
+ <button
545
+ onClick={() => createNewTab('python')}
546
+ className="block w-full px-3 py-2 text-sm text-gray-300 hover:bg-[#2d2d2d] text-left"
547
+ >
548
+ Python
549
+ </button>
550
+ <button
551
+ onClick={() => createNewTab('javascript')}
552
+ className="block w-full px-3 py-2 text-sm text-gray-300 hover:bg-[#2d2d2d] text-left"
553
+ >
554
+ JavaScript
555
+ </button>
556
+ <button
557
+ onClick={() => createNewTab('typescript')}
558
+ className="block w-full px-3 py-2 text-sm text-gray-300 hover:bg-[#2d2d2d] text-left"
559
+ >
560
+ TypeScript
561
+ </button>
562
+ <button
563
+ onClick={() => createNewTab('react')}
564
+ className="block w-full px-3 py-2 text-sm text-gray-300 hover:bg-[#2d2d2d] text-left"
565
+ >
566
+ React
567
+ </button>
568
+ <button
569
+ onClick={() => createNewTab('flutter')}
570
+ className="block w-full px-3 py-2 text-sm text-gray-300 hover:bg-[#2d2d2d] text-left"
571
+ >
572
+ Flutter/Dart
573
+ </button>
574
+ <button
575
+ onClick={() => createNewTab('html')}
576
+ className="block w-full px-3 py-2 text-sm text-gray-300 hover:bg-[#2d2d2d] text-left"
577
+ >
578
+ HTML
579
+ </button>
580
+ <button
581
+ onClick={() => createNewTab('css')}
582
+ className="block w-full px-3 py-2 text-sm text-gray-300 hover:bg-[#2d2d2d] text-left"
583
+ >
584
+ CSS
585
+ </button>
586
+ </div>
587
+ </div>
588
+ </div>
589
+
590
+ {/* Monaco Editor */}
591
+ <div className={`flex-1 ${showConsole ? 'h-3/5' : ''}`}>
592
+ <Editor
593
+ height="100%"
594
+ language={activeTabContent?.language || 'python'}
595
+ value={activeTabContent?.content || ''}
596
+ onChange={handleEditorChange}
597
+ theme="vs-dark"
598
+ options={{
599
+ minimap: { enabled: true },
600
+ fontSize: 14,
601
+ wordWrap: 'on',
602
+ automaticLayout: true,
603
+ scrollBeyondLastLine: false,
604
+ tabSize: 4
605
+ }}
606
+ />
607
+ </div>
608
+
609
+ {/* Console Output */}
610
+ {showConsole && (
611
+ <div className="h-2/5 bg-[#1e1e1e] border-t border-[#3e3e3e] flex flex-col">
612
+ <div className="flex items-center justify-between px-3 py-1 bg-[#252526] border-b border-[#3e3e3e]">
613
+ <span className="text-xs text-gray-400">Console Output</span>
614
+ <button
615
+ onClick={() => setExecutionResults([])}
616
+ className="text-xs text-gray-400 hover:text-white"
617
+ >
618
+ Clear
619
+ </button>
620
+ </div>
621
+ <div className="flex-1 overflow-y-auto p-3 font-mono text-xs">
622
+ {executionResults.map((result, index) => (
623
+ <div key={index} className="mb-2">
624
+ <div className="text-gray-500 text-xs mb-1">
625
+ [{new Date(result.timestamp).toLocaleTimeString()}]
626
+ </div>
627
+ {result.error ? (
628
+ <div className="text-red-400">{result.error}</div>
629
+ ) : (
630
+ <div className="text-green-400 whitespace-pre-wrap">{result.output}</div>
631
+ )}
632
+ </div>
633
+ ))}
634
+ </div>
635
+ </div>
636
+ )}
637
+ </div>
638
+
639
+ {/* Preview Section (for HTML/CSS/JS) */}
640
+ {showPreview && (activeTabContent?.type === 'html' || activeTabContent?.type === 'css' || activeTabContent?.type === 'javascript') && (
641
+ <div className="flex-1 flex flex-col bg-white">
642
+ <div className="bg-gray-100 px-4 py-2 border-b border-gray-300 flex items-center justify-between">
643
+ <span className="text-sm font-medium">Live Preview</span>
644
+ <button
645
+ onClick={() => {
646
+ if (previewRef.current) {
647
+ previewRef.current.src = previewRef.current.src
648
+ }
649
+ }}
650
+ className="p-1 hover:bg-gray-200 rounded"
651
+ >
652
+ <Play size={16} className="text-gray-600" />
653
+ </button>
654
+ </div>
655
+ <iframe
656
+ ref={previewRef}
657
+ className="flex-1 w-full bg-white"
658
+ sandbox="allow-scripts"
659
+ srcDoc={activeTabContent?.content}
660
+ title="Code Preview"
661
+ />
662
+ </div>
663
+ )}
664
+ </div>
665
+ </div>
666
+ </Window>
667
+ )
668
+ }
app/components/ContextMenu.tsx ADDED
@@ -0,0 +1,90 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ 'use client'
2
+
3
+ import React from 'react'
4
+ import { motion, AnimatePresence } from 'framer-motion'
5
+ import {
6
+ FolderPlus,
7
+ Info,
8
+ Image,
9
+ Broom,
10
+ ArrowsClockwise,
11
+ Desktop
12
+ } from '@phosphor-icons/react'
13
+
14
+ interface ContextMenuProps {
15
+ isOpen: boolean
16
+ position: { x: number; y: number }
17
+ onClose: () => void
18
+ onAction: (action: string) => void
19
+ }
20
+
21
+ export function ContextMenu({ isOpen, position, onClose, onAction }: ContextMenuProps) {
22
+ const menuItems = [
23
+ { id: 'new-folder', label: 'New Folder', icon: FolderPlus },
24
+ { id: 'get-info', label: 'Get Info', icon: Info },
25
+ { type: 'separator' },
26
+ { id: 'change-wallpaper', label: 'Change Wallpaper', icon: Image },
27
+ { id: 'clean-up', label: 'Clean Up', icon: Broom },
28
+ { id: 'refresh', label: 'Refresh', icon: ArrowsClockwise },
29
+ { type: 'separator' },
30
+ { id: 'display-settings', label: 'Display Settings', icon: Desktop },
31
+ ]
32
+
33
+ const handleAction = (actionId: string) => {
34
+ onAction(actionId)
35
+ onClose()
36
+ }
37
+
38
+ return (
39
+ <AnimatePresence>
40
+ {isOpen && (
41
+ <>
42
+ {/* Invisible backdrop to detect outside clicks */}
43
+ <div
44
+ className="fixed inset-0 z-[79]"
45
+ onClick={onClose}
46
+ />
47
+
48
+ {/* Context Menu */}
49
+ <motion.div
50
+ initial={{ opacity: 0, scale: 0.95 }}
51
+ animate={{ opacity: 1, scale: 1 }}
52
+ exit={{ opacity: 0, scale: 0.95 }}
53
+ transition={{ duration: 0.1 }}
54
+ className="fixed glass rounded-lg shadow-xl py-1 z-[80] min-w-[200px]"
55
+ style={{
56
+ left: position.x,
57
+ top: position.y,
58
+ // Prevent menu from going off-screen
59
+ maxWidth: `calc(100vw - ${position.x}px - 20px)`,
60
+ maxHeight: `calc(100vh - ${position.y}px - 20px)`
61
+ }}
62
+ >
63
+ {menuItems.map((item, index) => {
64
+ if (item.type === 'separator') {
65
+ return (
66
+ <div
67
+ key={`separator-${index}`}
68
+ className="h-px bg-gray-300/50 my-1 mx-2"
69
+ />
70
+ )
71
+ }
72
+
73
+ const Icon = item.icon!
74
+ return (
75
+ <button
76
+ key={item.id}
77
+ onClick={() => handleAction(item.id!)}
78
+ className="w-full text-left px-4 py-1.5 hover:bg-blue-500 hover:text-white transition-colors flex items-center gap-3 text-sm"
79
+ >
80
+ <Icon size={16} weight="regular" />
81
+ <span>{item.label}</span>
82
+ </button>
83
+ )
84
+ })}
85
+ </motion.div>
86
+ </>
87
+ )}
88
+ </AnimatePresence>
89
+ )
90
+ }
app/components/Desktop.tsx ADDED
@@ -0,0 +1,498 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ 'use client'
2
+
3
+ import React, { useState, useEffect } from 'react'
4
+ import { Dock } from './Dock'
5
+ import { TopBar } from './TopBar'
6
+ import { FileManager } from './FileManager'
7
+ import { Calendar } from './Calendar'
8
+ import { DraggableDesktopIcon } from './DraggableDesktopIcon'
9
+ import { MatrixRain } from './MatrixRain'
10
+ import { HelpModal } from './HelpModal'
11
+ import { DesktopContextMenu } from './DesktopContextMenu'
12
+ import { BackgroundSelector } from './BackgroundSelector'
13
+ import WebBrowserApp from './WebBrowserApp'
14
+ import { GeminiChat } from './GeminiChat'
15
+ import { Clock } from './Clock'
16
+ import { Terminal } from './Terminal'
17
+ import { SpotlightSearch } from './SpotlightSearch'
18
+ import { ContextMenu } from './ContextMenu'
19
+ import { VSCodeEditor } from './VSCodeEditor'
20
+ import { CodePlayground } from './CodePlayground'
21
+ import { AboutModal } from './AboutModal'
22
+ import { CodeExecutor } from './CodeExecutor'
23
+ import { motion } from 'framer-motion'
24
+
25
+ export function Desktop() {
26
+ const [fileManagerOpen, setFileManagerOpen] = useState(true)
27
+ const [calendarOpen, setCalendarOpen] = useState(false)
28
+ const [clockOpen, setClockOpen] = useState(false)
29
+ const [browserOpen, setBrowserOpen] = useState(false)
30
+ const [geminiChatOpen, setGeminiChatOpen] = useState(false)
31
+ const [terminalOpen, setTerminalOpen] = useState(false)
32
+ const [vscodeOpen, setVscodeOpen] = useState(false)
33
+ const [codePlaygroundOpen, setCodePlaygroundOpen] = useState(false)
34
+ const [spotlightOpen, setSpotlightOpen] = useState(false)
35
+ const [contextMenuVisible, setContextMenuVisible] = useState(false)
36
+ const [contextMenuPosition, setContextMenuPosition] = useState({ x: 0, y: 0 })
37
+ const [userSession, setUserSession] = useState<string>(`session_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`)
38
+ const [currentPath, setCurrentPath] = useState('')
39
+ const [matrixActive, setMatrixActive] = useState(false)
40
+ const [helpModalOpen, setHelpModalOpen] = useState(false)
41
+ const [contextMenuOpen, setContextMenuOpen] = useState(false)
42
+ const [contextMenuPos, setContextMenuPos] = useState({ x: 0, y: 0 })
43
+ const [backgroundSelectorOpen, setBackgroundSelectorOpen] = useState(false)
44
+ const [currentBackground, setCurrentBackground] = useState('/background.webp')
45
+ const [aboutModalOpen, setAboutModalOpen] = useState(false)
46
+ const [codeExecutorOpen, setCodeExecutorOpen] = useState(false)
47
+
48
+ const openFileManager = (path: string) => {
49
+ setCurrentPath(path)
50
+ setFileManagerOpen(true)
51
+ }
52
+
53
+ const closeFileManager = () => {
54
+ setFileManagerOpen(false)
55
+ }
56
+
57
+ const openCalendar = () => {
58
+ setCalendarOpen(true)
59
+ }
60
+
61
+ const closeCalendar = () => {
62
+ setCalendarOpen(false)
63
+ }
64
+
65
+ const openClock = () => {
66
+ setClockOpen(true)
67
+ }
68
+
69
+ const closeClock = () => {
70
+ setClockOpen(false)
71
+ }
72
+
73
+ const openBrowser = () => {
74
+ setBrowserOpen(true)
75
+ }
76
+
77
+ const closeBrowser = () => {
78
+ setBrowserOpen(false)
79
+ }
80
+
81
+ const openGeminiChat = () => {
82
+ setGeminiChatOpen(true)
83
+ }
84
+
85
+ const closeGeminiChat = () => {
86
+ setGeminiChatOpen(false)
87
+ }
88
+
89
+ const openTerminal = () => {
90
+ setTerminalOpen(true)
91
+ }
92
+
93
+ const closeTerminal = () => {
94
+ setTerminalOpen(false)
95
+ }
96
+
97
+ const openVSCode = () => {
98
+ setVscodeOpen(true)
99
+ }
100
+
101
+ const closeVSCode = () => {
102
+ setVscodeOpen(false)
103
+ }
104
+
105
+ const openCodePlayground = () => {
106
+ setCodePlaygroundOpen(true)
107
+ }
108
+
109
+ const closeCodePlayground = () => {
110
+ setCodePlaygroundOpen(false)
111
+ }
112
+
113
+ const openCodeExecutor = () => {
114
+ setCodeExecutorOpen(true)
115
+ }
116
+
117
+ const closeCodeExecutor = () => {
118
+ setCodeExecutorOpen(false)
119
+ }
120
+
121
+ const handleOpenApp = (appId: string) => {
122
+ switch(appId) {
123
+ case 'files':
124
+ openFileManager('')
125
+ break
126
+ case 'calendar':
127
+ openCalendar()
128
+ break
129
+ case 'clock':
130
+ openClock()
131
+ break
132
+ case 'browser':
133
+ openBrowser()
134
+ break
135
+ case 'gemini':
136
+ openGeminiChat()
137
+ break
138
+ case 'terminal':
139
+ openTerminal()
140
+ break
141
+ case 'vscode':
142
+ openVSCode()
143
+ break
144
+ case 'playground':
145
+ openCodePlayground()
146
+ break
147
+ case 'executor':
148
+ openCodeExecutor()
149
+ break
150
+ }
151
+ }
152
+
153
+ const handleContextMenuAction = (action: string) => {
154
+ switch(action) {
155
+ case 'change-wallpaper':
156
+ setBackgroundSelectorOpen(true)
157
+ break
158
+ case 'new-folder':
159
+ // Add folder creation logic if needed
160
+ break
161
+ case 'refresh':
162
+ window.location.reload()
163
+ break
164
+ }
165
+ }
166
+
167
+ const triggerMatrix = () => {
168
+ setMatrixActive(true)
169
+ }
170
+
171
+ const handleMatrixComplete = () => {
172
+ setMatrixActive(false)
173
+ }
174
+
175
+ const openHelpModal = () => {
176
+ setHelpModalOpen(true)
177
+ }
178
+
179
+ const closeHelpModal = () => {
180
+ setHelpModalOpen(false)
181
+ }
182
+
183
+ const handleDesktopRightClick = (e: React.MouseEvent) => {
184
+ e.preventDefault()
185
+ setContextMenuPosition({ x: e.clientX, y: e.clientY })
186
+ setContextMenuVisible(true)
187
+ }
188
+
189
+ const handleChangeBackground = (type: 'upload' | 'preset') => {
190
+ setContextMenuOpen(false)
191
+ setBackgroundSelectorOpen(true)
192
+ }
193
+
194
+ const handleSelectBackground = (background: string | File) => {
195
+ if (typeof background === 'string') {
196
+ // Handle preset backgrounds
197
+ if (background.startsWith('gradient-')) {
198
+ setCurrentBackground(background)
199
+ } else {
200
+ setCurrentBackground(background)
201
+ }
202
+ } else {
203
+ // Handle uploaded file
204
+ const url = URL.createObjectURL(background)
205
+ setCurrentBackground(url)
206
+ }
207
+ }
208
+
209
+ // Keyboard shortcuts
210
+ useEffect(() => {
211
+ const handleKeyDown = (e: KeyboardEvent) => {
212
+ // Cmd/Ctrl + Space for Spotlight Search
213
+ if ((e.metaKey || e.ctrlKey) && e.key === ' ') {
214
+ e.preventDefault()
215
+ setSpotlightOpen(true)
216
+ }
217
+ // Cmd/Ctrl + K for Spotlight Search (alternative)
218
+ if ((e.metaKey || e.ctrlKey) && e.key === 'k') {
219
+ e.preventDefault()
220
+ setSpotlightOpen(true)
221
+ }
222
+ }
223
+
224
+ window.addEventListener('keydown', handleKeyDown)
225
+ return () => window.removeEventListener('keydown', handleKeyDown)
226
+ }, [])
227
+
228
+ const getBackgroundStyle = () => {
229
+ if (currentBackground.startsWith('gradient-')) {
230
+ const gradients: Record<string, string> = {
231
+ 'gradient-purple': 'linear-gradient(135deg, #77216F 0%, #5E2750 50%, #2C001E 100%)',
232
+ 'gradient-blue': 'linear-gradient(135deg, #1e3c72 0%, #2a5298 50%, #7e8ba3 100%)',
233
+ 'gradient-green': 'linear-gradient(135deg, #134e5e 0%, #71b280 50%, #a8e063 100%)',
234
+ 'gradient-orange': 'linear-gradient(135deg, #ff512f 0%, #dd2476 50%, #f09819 100%)',
235
+ 'gradient-dark': 'linear-gradient(135deg, #0f0f0f 0%, #1a1a1a 50%, #2d2d2d 100%)',
236
+ 'gradient-cosmic': 'linear-gradient(135deg, #667eea 0%, #764ba2 50%, #f093fb 100%)'
237
+ }
238
+ return { background: gradients[currentBackground] || gradients['gradient-purple'] }
239
+ } else {
240
+ return {
241
+ backgroundImage: `url('${currentBackground}')`,
242
+ backgroundSize: 'cover',
243
+ backgroundPosition: 'center',
244
+ backgroundRepeat: 'no-repeat'
245
+ }
246
+ }
247
+ }
248
+
249
+ return (
250
+ <div className="relative h-screen w-screen overflow-hidden flex" onContextMenu={handleDesktopRightClick}>
251
+ <div
252
+ className="absolute inset-0"
253
+ style={getBackgroundStyle()}
254
+ >
255
+ {/* Overlay for better text visibility */}
256
+ {!currentBackground.startsWith('gradient-') && (
257
+ <div
258
+ className="absolute inset-0 bg-black/20"
259
+ style={{
260
+ mixBlendMode: 'multiply'
261
+ }}
262
+ />
263
+ )}
264
+ <svg className="absolute inset-0 w-full h-full opacity-10" xmlns="http://www.w3.org/2000/svg">
265
+ <defs>
266
+ <pattern id="ubuntu-pattern" x="0" y="0" width="100" height="100" patternUnits="userSpaceOnUse">
267
+ <circle cx="50" cy="50" r="2" fill="white" opacity="0.3"/>
268
+ </pattern>
269
+ </defs>
270
+ <rect width="100%" height="100%" fill="url(#ubuntu-pattern)" />
271
+ </svg>
272
+ </div>
273
+
274
+ <TopBar onPowerAction={triggerMatrix} onAboutClick={() => setAboutModalOpen(true)} />
275
+
276
+ <Dock onOpenFileManager={openFileManager} onOpenCalendar={openCalendar} onOpenClock={openClock} onOpenBrowser={openBrowser} onOpenGeminiChat={openGeminiChat} onOpenVSCode={openVSCode} onOpenCodePlayground={openCodePlayground} />
277
+
278
+ <div className="flex-1">
279
+ {/* Desktop Icons - Positioned in a grid layout */}
280
+ <div className="absolute top-20 left-8">
281
+ <DraggableDesktopIcon
282
+ id="files"
283
+ label="Files"
284
+ iconType="files"
285
+ initialPosition={{ x: 20, y: 20 }}
286
+ onClick={() => {}}
287
+ onDoubleClick={() => openFileManager('')}
288
+ />
289
+ <DraggableDesktopIcon
290
+ id="browser"
291
+ label="Browser"
292
+ iconType="browser"
293
+ initialPosition={{ x: 120, y: 20 }}
294
+ onClick={() => {}}
295
+ onDoubleClick={openBrowser}
296
+ />
297
+ <DraggableDesktopIcon
298
+ id="gemini"
299
+ label="Gemini"
300
+ iconType="gemini"
301
+ initialPosition={{ x: 220, y: 20 }}
302
+ onClick={() => {}}
303
+ onDoubleClick={openGeminiChat}
304
+ />
305
+ <DraggableDesktopIcon
306
+ id="clock"
307
+ label="Clock"
308
+ iconType="clock"
309
+ initialPosition={{ x: 20, y: 120 }}
310
+ onClick={() => {}}
311
+ onDoubleClick={openClock}
312
+ />
313
+ <DraggableDesktopIcon
314
+ id="calendar"
315
+ label="Calendar"
316
+ iconType="calendar"
317
+ initialPosition={{ x: 120, y: 120 }}
318
+ onClick={() => {}}
319
+ onDoubleClick={openCalendar}
320
+ />
321
+ <DraggableDesktopIcon
322
+ id="terminal"
323
+ label="Terminal"
324
+ iconType="terminal"
325
+ initialPosition={{ x: 20, y: 220 }}
326
+ onClick={() => {}}
327
+ onDoubleClick={openTerminal}
328
+ />
329
+ <DraggableDesktopIcon
330
+ id="harddrive"
331
+ label="System"
332
+ iconType="harddrive"
333
+ initialPosition={{ x: 120, y: 220 }}
334
+ onClick={() => {}}
335
+ onDoubleClick={() => openFileManager('')}
336
+ />
337
+ <DraggableDesktopIcon
338
+ id="vscode"
339
+ label="VS Code"
340
+ iconType="vscode"
341
+ initialPosition={{ x: 320, y: 20 }}
342
+ onClick={() => {}}
343
+ onDoubleClick={openVSCode}
344
+ />
345
+ <DraggableDesktopIcon
346
+ id="playground"
347
+ label="Code Playground"
348
+ iconType="playground"
349
+ initialPosition={{ x: 220, y: 220 }}
350
+ onClick={() => {}}
351
+ onDoubleClick={openCodePlayground}
352
+ />
353
+ <DraggableDesktopIcon
354
+ id="executor"
355
+ label="Code Executor"
356
+ iconType="terminal"
357
+ initialPosition={{ x: 320, y: 120 }}
358
+ onClick={() => {}}
359
+ onDoubleClick={openCodeExecutor}
360
+ />
361
+ </div>
362
+
363
+ {fileManagerOpen && (
364
+ <motion.div
365
+ initial={{ scale: 0.95, opacity: 0 }}
366
+ animate={{ scale: 1, opacity: 1 }}
367
+ transition={{ duration: 0.15 }}
368
+ >
369
+ <FileManager
370
+ currentPath={currentPath}
371
+ onNavigate={setCurrentPath}
372
+ onClose={closeFileManager}
373
+ />
374
+ </motion.div>
375
+ )}
376
+
377
+ {calendarOpen && (
378
+ <motion.div
379
+ initial={{ scale: 0.95, opacity: 0 }}
380
+ animate={{ scale: 1, opacity: 1 }}
381
+ transition={{ duration: 0.15 }}
382
+ >
383
+ <Calendar onClose={closeCalendar} />
384
+ </motion.div>
385
+ )}
386
+
387
+ {clockOpen && (
388
+ <motion.div
389
+ initial={{ scale: 0.95, opacity: 0 }}
390
+ animate={{ scale: 1, opacity: 1 }}
391
+ transition={{ duration: 0.15 }}
392
+ >
393
+ <Clock onClose={closeClock} />
394
+ </motion.div>
395
+ )}
396
+
397
+ {browserOpen && (
398
+ <motion.div
399
+ initial={{ scale: 0.95, opacity: 0 }}
400
+ animate={{ scale: 1, opacity: 1 }}
401
+ transition={{ duration: 0.15 }}
402
+ >
403
+ <WebBrowserApp onClose={closeBrowser} />
404
+ </motion.div>
405
+ )}
406
+
407
+ {geminiChatOpen && (
408
+ <GeminiChat onClose={closeGeminiChat} />
409
+ )}
410
+
411
+ {terminalOpen && (
412
+ <motion.div
413
+ initial={{ scale: 0.95, opacity: 0 }}
414
+ animate={{ scale: 1, opacity: 1 }}
415
+ transition={{ duration: 0.15 }}
416
+ >
417
+ <Terminal onClose={closeTerminal} />
418
+ </motion.div>
419
+ )}
420
+
421
+ {vscodeOpen && (
422
+ <motion.div
423
+ initial={{ scale: 0.95, opacity: 0 }}
424
+ animate={{ scale: 1, opacity: 1 }}
425
+ transition={{ duration: 0.15 }}
426
+ >
427
+ <VSCodeEditor onClose={closeVSCode} userSession={userSession} />
428
+ </motion.div>
429
+ )}
430
+
431
+ {codePlaygroundOpen && (
432
+ <motion.div
433
+ initial={{ scale: 0.95, opacity: 0 }}
434
+ animate={{ scale: 1, opacity: 1 }}
435
+ transition={{ duration: 0.15 }}
436
+ >
437
+ <CodePlayground onClose={closeCodePlayground} userSession={userSession} />
438
+ </motion.div>
439
+ )}
440
+
441
+ {codeExecutorOpen && (
442
+ <motion.div
443
+ initial={{ scale: 0.95, opacity: 0 }}
444
+ animate={{ scale: 1, opacity: 1 }}
445
+ transition={{ duration: 0.15 }}
446
+ >
447
+ <CodeExecutor onClose={closeCodeExecutor} />
448
+ </motion.div>
449
+ )}
450
+ </div>
451
+
452
+ {/* Spotlight Search */}
453
+ <SpotlightSearch
454
+ isOpen={spotlightOpen}
455
+ onClose={() => setSpotlightOpen(false)}
456
+ onOpenApp={handleOpenApp}
457
+ />
458
+
459
+ {/* Context Menu */}
460
+ <ContextMenu
461
+ isOpen={contextMenuVisible}
462
+ position={contextMenuPosition}
463
+ onClose={() => setContextMenuVisible(false)}
464
+ onAction={handleContextMenuAction}
465
+ />
466
+
467
+ {/* Matrix Rain Effect */}
468
+ <div onClick={handleMatrixComplete} style={{ cursor: matrixActive ? 'pointer' : 'default' }}>
469
+ <MatrixRain isActive={matrixActive} onComplete={handleMatrixComplete} />
470
+ </div>
471
+
472
+ {/* Help Modal */}
473
+ <HelpModal isOpen={helpModalOpen} onClose={closeHelpModal} />
474
+
475
+ {/* Desktop Context Menu */}
476
+ <DesktopContextMenu
477
+ isOpen={contextMenuOpen}
478
+ position={contextMenuPos}
479
+ onClose={() => setContextMenuOpen(false)}
480
+ onChangeBackground={handleChangeBackground}
481
+ />
482
+
483
+ {/* Background Selector Modal */}
484
+ <BackgroundSelector
485
+ isOpen={backgroundSelectorOpen}
486
+ onClose={() => setBackgroundSelectorOpen(false)}
487
+ onSelectBackground={handleSelectBackground}
488
+ currentBackground={currentBackground}
489
+ />
490
+
491
+ {/* About Modal */}
492
+ <AboutModal
493
+ isOpen={aboutModalOpen}
494
+ onClose={() => setAboutModalOpen(false)}
495
+ />
496
+ </div>
497
+ )
498
+ }
app/components/DesktopContextMenu.tsx ADDED
@@ -0,0 +1,123 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ 'use client'
2
+
3
+ import React, { useRef, useEffect } from 'react'
4
+ import { motion, AnimatePresence } from 'framer-motion'
5
+ import {
6
+ Image,
7
+ PaintBrush,
8
+ Upload,
9
+ Palette,
10
+ Desktop as DesktopIcon,
11
+ Gradient
12
+ } from '@phosphor-icons/react'
13
+
14
+ interface DesktopContextMenuProps {
15
+ isOpen: boolean
16
+ position: { x: number; y: number }
17
+ onClose: () => void
18
+ onChangeBackground: (type: 'upload' | 'preset') => void
19
+ }
20
+
21
+ const presetBackgrounds = [
22
+ { name: 'Ubuntu Purple', value: 'gradient-purple' },
23
+ { name: 'Ocean Blue', value: 'gradient-blue' },
24
+ { name: 'Forest Green', value: 'gradient-green' },
25
+ { name: 'Sunset Orange', value: 'gradient-orange' },
26
+ { name: 'Dark Mode', value: 'gradient-dark' },
27
+ ]
28
+
29
+ export function DesktopContextMenu({
30
+ isOpen,
31
+ position,
32
+ onClose,
33
+ onChangeBackground
34
+ }: DesktopContextMenuProps) {
35
+ const menuRef = useRef<HTMLDivElement>(null)
36
+
37
+ useEffect(() => {
38
+ const handleClickOutside = (event: MouseEvent) => {
39
+ if (menuRef.current && !menuRef.current.contains(event.target as Node)) {
40
+ onClose()
41
+ }
42
+ }
43
+
44
+ const handleEscape = (event: KeyboardEvent) => {
45
+ if (event.key === 'Escape') {
46
+ onClose()
47
+ }
48
+ }
49
+
50
+ if (isOpen) {
51
+ document.addEventListener('mousedown', handleClickOutside)
52
+ document.addEventListener('keydown', handleEscape)
53
+ }
54
+
55
+ return () => {
56
+ document.removeEventListener('mousedown', handleClickOutside)
57
+ document.removeEventListener('keydown', handleEscape)
58
+ }
59
+ }, [isOpen, onClose])
60
+
61
+ return (
62
+ <AnimatePresence>
63
+ {isOpen && (
64
+ <motion.div
65
+ ref={menuRef}
66
+ initial={{ opacity: 0, scale: 0.95 }}
67
+ animate={{ opacity: 1, scale: 1 }}
68
+ exit={{ opacity: 0, scale: 0.95 }}
69
+ transition={{ duration: 0.1 }}
70
+ className="fixed z-50 bg-[#2C2C2C]/95 backdrop-blur-md rounded-lg shadow-2xl border border-white/10 py-2 min-w-[240px]"
71
+ style={{
72
+ left: `${position.x}px`,
73
+ top: `${position.y}px`,
74
+ maxHeight: 'calc(100vh - 20px)',
75
+ overflowY: 'auto'
76
+ }}
77
+ >
78
+ {/* Change Background Section */}
79
+ <div className="px-3 py-1.5 text-xs font-medium text-gray-400 uppercase tracking-wider">
80
+ Change Background
81
+ </div>
82
+
83
+ <button
84
+ onClick={() => onChangeBackground('upload')}
85
+ className="w-full px-3 py-2 flex items-center gap-3 hover:bg-white/10 transition-colors text-left"
86
+ >
87
+ <Upload size={18} className="text-blue-400" />
88
+ <span className="text-sm text-gray-200">Upload Image</span>
89
+ </button>
90
+
91
+ <button
92
+ onClick={() => onChangeBackground('preset')}
93
+ className="w-full px-3 py-2 flex items-center gap-3 hover:bg-white/10 transition-colors text-left"
94
+ >
95
+ <Palette size={18} className="text-purple-400" />
96
+ <span className="text-sm text-gray-200">Choose from Gallery</span>
97
+ </button>
98
+
99
+ <div className="h-px bg-white/10 my-2" />
100
+
101
+ {/* Display Settings */}
102
+ <div className="px-3 py-1.5 text-xs font-medium text-gray-400 uppercase tracking-wider">
103
+ Display
104
+ </div>
105
+
106
+ <button
107
+ className="w-full px-3 py-2 flex items-center gap-3 hover:bg-white/10 transition-colors text-left"
108
+ >
109
+ <DesktopIcon size={18} className="text-green-400" />
110
+ <span className="text-sm text-gray-200">Desktop Icons</span>
111
+ </button>
112
+
113
+ <button
114
+ className="w-full px-3 py-2 flex items-center gap-3 hover:bg-white/10 transition-colors text-left"
115
+ >
116
+ <Gradient size={18} className="text-orange-400" />
117
+ <span className="text-sm text-gray-200">Visual Effects</span>
118
+ </button>
119
+ </motion.div>
120
+ )}
121
+ </AnimatePresence>
122
+ )
123
+ }
app/components/DesktopIcon.tsx ADDED
@@ -0,0 +1,64 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ 'use client'
2
+
3
+ import React from 'react'
4
+ import { House, Trash, Calendar as CalendarIcon, Clock, Folder, Globe, Sparkle, GameController } from '@phosphor-icons/react'
5
+
6
+ interface DesktopIconProps {
7
+ icon: 'home' | 'trash' | 'calendar' | 'clock' | 'game' | 'folder' | 'browser' | 'gemini'
8
+ label: string
9
+ onClick: () => void
10
+ }
11
+
12
+ export function DesktopIcon({ icon, label, onClick }: DesktopIconProps) {
13
+ const getIcon = () => {
14
+ switch(icon) {
15
+ case 'home': return House
16
+ case 'calendar': return CalendarIcon
17
+ case 'clock': return Clock
18
+ case 'game': return GameController
19
+ case 'folder': return Folder
20
+ case 'browser': return Globe
21
+ case 'gemini': return Sparkle
22
+ case 'trash': return Trash
23
+ default: return Folder
24
+ }
25
+ }
26
+
27
+ const Icon = getIcon()
28
+
29
+ const getGradient = () => {
30
+ if (icon === 'calendar') {
31
+ return 'from-purple-500/80 to-purple-700/80 group-hover:from-purple-400/80 group-hover:to-purple-600/80'
32
+ }
33
+ if (icon === 'clock') {
34
+ return 'from-teal-500/80 to-cyan-600/80 group-hover:from-teal-400/80 group-hover:to-cyan-500/80'
35
+ }
36
+ if (icon === 'game') {
37
+ return 'from-pink-500/80 to-rose-600/80 group-hover:from-pink-400/80 group-hover:to-rose-500/80'
38
+ }
39
+ if (icon === 'folder') {
40
+ return 'from-orange-500/80 to-orange-700/80 group-hover:from-orange-400/80 group-hover:to-orange-600/80'
41
+ }
42
+ if (icon === 'browser') {
43
+ return 'from-blue-500/80 to-cyan-600/80 group-hover:from-blue-400/80 group-hover:to-cyan-500/80'
44
+ }
45
+ if (icon === 'gemini') {
46
+ return 'from-[#E95420]/80 to-[#d14818]/80 group-hover:from-[#E95420] group-hover:to-[#d14818]'
47
+ }
48
+ return 'from-gray-600/80 to-gray-700/80 group-hover:from-gray-500/80 group-hover:to-gray-600/80'
49
+ }
50
+
51
+ return (
52
+ <button
53
+ onClick={onClick}
54
+ className="flex flex-col items-center gap-1 p-2 rounded hover:bg-white/10 transition-colors w-24 group mb-2"
55
+ >
56
+ <div className={`w-16 h-16 rounded-lg bg-gradient-to-br ${getGradient()} backdrop-blur-sm flex items-center justify-center transition-all`}>
57
+ <Icon size={32} weight="regular" className="text-white" />
58
+ </div>
59
+ <span className="text-white text-xs text-center drop-shadow-[0_1px_2px_rgba(0,0,0,0.8)] font-medium">
60
+ {label}
61
+ </span>
62
+ </button>
63
+ )
64
+ }
app/components/Dock.tsx ADDED
@@ -0,0 +1,170 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ 'use client'
2
+
3
+ import React, { useState } from 'react'
4
+ import {
5
+ Folder,
6
+ Calendar as CalendarIcon,
7
+ Clock as ClockIcon,
8
+ Globe,
9
+ Sparkle,
10
+ Trash,
11
+ FolderOpen,
12
+ Compass,
13
+ Code,
14
+ Lightning
15
+ } from '@phosphor-icons/react'
16
+
17
+ interface DockProps {
18
+ onOpenFileManager: (path: string) => void
19
+ onOpenCalendar: () => void
20
+ onOpenClock: () => void
21
+ onOpenBrowser: () => void
22
+ onOpenGeminiChat: () => void
23
+ onOpenVSCode?: () => void
24
+ onOpenCodePlayground?: () => void
25
+ }
26
+
27
+ interface DockItemProps {
28
+ icon: React.ReactNode
29
+ label: string
30
+ onClick: () => void
31
+ isActive?: boolean
32
+ className?: string
33
+ }
34
+
35
+ const DockItem: React.FC<DockItemProps> = ({ icon, label, onClick, isActive = false, className = '' }) => {
36
+ const [isHovered, setIsHovered] = useState(false)
37
+
38
+ return (
39
+ <div className="dock-item group">
40
+ <button
41
+ onMouseEnter={() => setIsHovered(true)}
42
+ onMouseLeave={() => setIsHovered(false)}
43
+ onClick={onClick}
44
+ className={`app-icon w-12 h-12 rounded-xl shadow-lg flex items-center justify-center cursor-pointer border transition-all ${className}`}
45
+ title={label}
46
+ >
47
+ {icon}
48
+ </button>
49
+ <div
50
+ className={`dock-dot ${isActive ? 'opacity-100' : ''}`}
51
+ id={`dot-${label.toLowerCase().replace(/\s+/g, '-')}`}
52
+ />
53
+ </div>
54
+ )
55
+ }
56
+
57
+ export function Dock({
58
+ onOpenFileManager,
59
+ onOpenCalendar,
60
+ onOpenClock,
61
+ onOpenBrowser,
62
+ onOpenGeminiChat,
63
+ onOpenVSCode,
64
+ onOpenCodePlayground
65
+ }: DockProps) {
66
+ const dockItems = [
67
+ {
68
+ icon: (
69
+ <div className="bg-gradient-to-br from-blue-400 to-cyan-200 w-full h-full rounded-xl flex items-center justify-center border border-white/30">
70
+ <Folder size={28} weight="regular" className="text-blue-900" />
71
+ </div>
72
+ ),
73
+ label: 'Files',
74
+ onClick: () => onOpenFileManager(''),
75
+ className: ''
76
+ },
77
+ {
78
+ icon: (
79
+ <div className="bg-white w-full h-full rounded-xl overflow-hidden relative flex items-center justify-center">
80
+ <div className="absolute inset-0 bg-gradient-to-tr from-blue-500 to-cyan-300" />
81
+ <Compass size={36} weight="light" className="text-white relative z-10" />
82
+ </div>
83
+ ),
84
+ label: 'Browser',
85
+ onClick: onOpenBrowser,
86
+ className: ''
87
+ },
88
+ {
89
+ icon: (
90
+ <div className="bg-white w-full h-full rounded-xl flex items-center justify-center border border-gray-200">
91
+ <Sparkle size={28} weight="fill" className="text-blue-500" />
92
+ </div>
93
+ ),
94
+ label: 'Gemini',
95
+ onClick: onOpenGeminiChat,
96
+ className: ''
97
+ },
98
+ {
99
+ icon: (
100
+ <div className="bg-black w-full h-full rounded-full flex items-center justify-center border border-gray-600">
101
+ <div className="w-10 h-10 rounded-full border border-gray-600 relative bg-black">
102
+ <div className="absolute top-1/2 left-1/2 w-0.5 h-2.5 bg-white origin-bottom -translate-x-1/2 -translate-y-full" />
103
+ <div className="absolute top-1/2 left-1/2 w-0.5 h-3.5 bg-gray-300 origin-bottom -translate-x-1/2 -translate-y-full" />
104
+ <div className="absolute top-1/2 left-1/2 w-0.5 h-4 bg-orange-500 origin-bottom -translate-x-1/2 -translate-y-full animate-spin" style={{ animationDuration: '60s' }} />
105
+ </div>
106
+ </div>
107
+ ),
108
+ label: 'Clock',
109
+ onClick: onOpenClock,
110
+ className: 'rounded-full'
111
+ },
112
+ {
113
+ icon: (
114
+ <div className="bg-gradient-to-br from-purple-500 to-purple-600 w-full h-full rounded-xl flex items-center justify-center shadow-inner">
115
+ <CalendarIcon size={28} weight="regular" className="text-white" />
116
+ </div>
117
+ ),
118
+ label: 'Calendar',
119
+ onClick: onOpenCalendar,
120
+ className: ''
121
+ },
122
+ {
123
+ icon: (
124
+ <div className="bg-gradient-to-br from-blue-600 to-blue-800 w-full h-full rounded-xl flex items-center justify-center border border-blue-900/30 shadow-inner">
125
+ <Code size={28} weight="bold" className="text-white" />
126
+ </div>
127
+ ),
128
+ label: 'VS Code',
129
+ onClick: onOpenVSCode || (() => {}),
130
+ className: ''
131
+ },
132
+ {
133
+ icon: (
134
+ <div className="bg-gradient-to-br from-yellow-500 to-orange-600 w-full h-full rounded-xl flex items-center justify-center border border-orange-700/30 shadow-inner">
135
+ <Lightning size={28} weight="fill" className="text-white" />
136
+ </div>
137
+ ),
138
+ label: 'Code Playground',
139
+ onClick: onOpenCodePlayground || (() => {}),
140
+ className: ''
141
+ }
142
+ ]
143
+
144
+ return (
145
+ <div className="dock-container">
146
+ <div className="dock-glass px-3 pb-2 pt-3 rounded-2xl flex items-end gap-2 shadow-2xl border border-white/20 transition-all duration-300">
147
+ {dockItems.map((item, index) => (
148
+ <React.Fragment key={item.label}>
149
+ <DockItem {...item} />
150
+ {index === dockItems.length - 1 && (
151
+ <>
152
+ <div className="w-px h-10 bg-gray-400/30 mx-1" />
153
+ <DockItem
154
+ icon={
155
+ <div className="bg-gradient-to-b from-gray-200 to-gray-300 w-full h-full rounded-xl flex items-center justify-center border border-white/40">
156
+ <Trash size={24} weight="regular" className="text-gray-600" />
157
+ </div>
158
+ }
159
+ label="Trash"
160
+ onClick={() => {}}
161
+ className=""
162
+ />
163
+ </>
164
+ )}
165
+ </React.Fragment>
166
+ ))}
167
+ </div>
168
+ </div>
169
+ )
170
+ }
app/components/DraggableDesktopIcon.tsx ADDED
@@ -0,0 +1,146 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ 'use client'
2
+
3
+ import React, { useState, useRef } from 'react'
4
+ import Draggable from 'react-draggable'
5
+ import {
6
+ Folder,
7
+ Calendar,
8
+ Clock,
9
+ Globe,
10
+ Sparkle,
11
+ Terminal,
12
+ HardDrives,
13
+ Compass,
14
+ Code,
15
+ Lightning
16
+ } from '@phosphor-icons/react'
17
+
18
+ interface DraggableDesktopIconProps {
19
+ id: string
20
+ label: string
21
+ iconType: string
22
+ initialPosition?: { x: number; y: number }
23
+ onClick: () => void
24
+ onDoubleClick: () => void
25
+ }
26
+
27
+ export function DraggableDesktopIcon({
28
+ id,
29
+ label,
30
+ iconType,
31
+ initialPosition = { x: 0, y: 0 },
32
+ onClick,
33
+ onDoubleClick
34
+ }: DraggableDesktopIconProps) {
35
+ const [selected, setSelected] = useState(false)
36
+ const nodeRef = useRef<HTMLDivElement>(null) as React.MutableRefObject<HTMLDivElement>
37
+
38
+ const handleClick = () => {
39
+ setSelected(true)
40
+ onClick()
41
+ // Deselect after a short time
42
+ setTimeout(() => setSelected(false), 3000)
43
+ }
44
+
45
+ const getIcon = () => {
46
+ switch (iconType) {
47
+ case 'files':
48
+ return (
49
+ <div className="bg-gradient-to-br from-blue-400 to-cyan-200 w-full h-full rounded-xl flex items-center justify-center border border-white/30">
50
+ <Folder size={32} weight="regular" className="text-blue-900" />
51
+ </div>
52
+ )
53
+ case 'calendar':
54
+ return (
55
+ <div className="bg-gradient-to-br from-purple-500 to-purple-600 w-full h-full rounded-xl flex items-center justify-center shadow-inner">
56
+ <Calendar size={32} weight="regular" className="text-white" />
57
+ </div>
58
+ )
59
+ case 'clock':
60
+ return (
61
+ <div className="bg-black w-full h-full rounded-full flex items-center justify-center border-2 border-gray-600">
62
+ <Clock size={32} weight="regular" className="text-white" />
63
+ </div>
64
+ )
65
+ case 'browser':
66
+ return (
67
+ <div className="bg-white w-full h-full rounded-xl overflow-hidden relative flex items-center justify-center">
68
+ <div className="absolute inset-0 bg-gradient-to-tr from-blue-500 to-cyan-300" />
69
+ <Compass size={36} weight="light" className="text-white relative z-10" />
70
+ </div>
71
+ )
72
+ case 'gemini':
73
+ return (
74
+ <div className="bg-white w-full h-full rounded-xl flex items-center justify-center border border-gray-200">
75
+ <Sparkle size={32} weight="fill" className="text-blue-500" />
76
+ </div>
77
+ )
78
+ case 'terminal':
79
+ return (
80
+ <div className="bg-gray-800 w-full h-full rounded-xl flex items-center justify-center border border-gray-600">
81
+ <span className="text-white font-mono font-bold text-xl">&gt;_</span>
82
+ </div>
83
+ )
84
+ case 'harddrive':
85
+ return (
86
+ <div className="bg-gray-200 w-full h-full rounded-lg flex items-center justify-center border border-gray-300 relative">
87
+ <HardDrives size={32} weight="regular" className="text-gray-600" />
88
+ <div className="absolute bottom-2 right-2 w-1.5 h-1.5 bg-green-400 rounded-full animate-pulse" />
89
+ </div>
90
+ )
91
+ case 'vscode':
92
+ return (
93
+ <div className="bg-gradient-to-br from-blue-600 to-blue-800 w-full h-full rounded-xl flex items-center justify-center border border-blue-900/30 shadow-inner">
94
+ <Code size={32} weight="bold" className="text-white" />
95
+ </div>
96
+ )
97
+ case 'playground':
98
+ return (
99
+ <div className="bg-gradient-to-br from-yellow-500 to-orange-600 w-full h-full rounded-xl flex items-center justify-center border border-orange-700/30 shadow-inner">
100
+ <Lightning size={32} weight="fill" className="text-white" />
101
+ </div>
102
+ )
103
+ default:
104
+ return (
105
+ <div className="bg-gray-400 w-full h-full rounded-xl flex items-center justify-center">
106
+ <Folder size={32} weight="regular" className="text-white" />
107
+ </div>
108
+ )
109
+ }
110
+ }
111
+
112
+ return (
113
+ <Draggable
114
+ nodeRef={nodeRef}
115
+ defaultPosition={initialPosition}
116
+ grid={[25, 25]}
117
+ handle=".icon-handle"
118
+ >
119
+ <div
120
+ ref={nodeRef}
121
+ className="absolute flex flex-col items-center w-20 group cursor-pointer transition-all hover:bg-white/10 rounded p-1"
122
+ onClick={handleClick}
123
+ onDoubleClick={onDoubleClick}
124
+ >
125
+ <div className="icon-handle">
126
+ <div
127
+ className={`w-14 h-14 shadow-lg transition-transform hover:scale-110 ${
128
+ iconType === 'clock' ? '' : 'rounded-xl'
129
+ }`}
130
+ >
131
+ {getIcon()}
132
+ </div>
133
+ </div>
134
+ <span
135
+ className={`mt-1 text-xs font-medium drop-shadow-lg text-center leading-tight px-1 py-0.5 rounded transition-colors ${
136
+ selected
137
+ ? 'bg-blue-600/80 text-white'
138
+ : 'text-white bg-black/20 group-hover:bg-blue-600/80'
139
+ }`}
140
+ >
141
+ {label}
142
+ </span>
143
+ </div>
144
+ </Draggable>
145
+ )
146
+ }
app/components/FileManager.tsx ADDED
@@ -0,0 +1,542 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ 'use client'
2
+
3
+ import React, { useState, useEffect } from 'react'
4
+ import {
5
+ X,
6
+ Minus,
7
+ Square,
8
+ CaretLeft,
9
+ CaretRight,
10
+ House,
11
+ MagnifyingGlass,
12
+ Folder as FolderIcon,
13
+ File,
14
+ Image as ImageIcon,
15
+ FilePdf,
16
+ FileDoc,
17
+ FileText,
18
+ Upload,
19
+ Download,
20
+ Trash,
21
+ Plus,
22
+ Eye,
23
+ Users,
24
+ Globe
25
+ } from '@phosphor-icons/react'
26
+ import { motion } from 'framer-motion'
27
+ import { FilePreview } from './FilePreview'
28
+
29
+ interface FileManagerProps {
30
+ currentPath: string
31
+ onNavigate: (path: string) => void
32
+ onClose: () => void
33
+ }
34
+
35
+ interface FileItem {
36
+ name: string
37
+ type: 'folder' | 'file'
38
+ size?: number
39
+ modified?: string
40
+ path: string
41
+ extension?: string
42
+ }
43
+
44
+ export function FileManager({ currentPath, onNavigate, onClose }: FileManagerProps) {
45
+ const [files, setFiles] = useState<FileItem[]>([])
46
+ const [loading, setLoading] = useState(true)
47
+ const [searchQuery, setSearchQuery] = useState('')
48
+ const [selectedFiles, setSelectedFiles] = useState<Set<string>>(new Set())
49
+ const [uploadModalOpen, setUploadModalOpen] = useState(false)
50
+ const [previewFile, setPreviewFile] = useState<FileItem | null>(null)
51
+ const [windowPos, setWindowPos] = useState({ x: 60, y: 60 })
52
+ const [isDragging, setIsDragging] = useState(false)
53
+ const [dragStart, setDragStart] = useState({ x: 0, y: 0 })
54
+ const [isPublicFolder, setIsPublicFolder] = useState(false)
55
+
56
+ // Load files when path changes
57
+ useEffect(() => {
58
+ // Check if this is the public folder
59
+ setIsPublicFolder(currentPath === 'public' || currentPath.startsWith('public/'))
60
+ loadFiles()
61
+ }, [currentPath])
62
+
63
+ const loadFiles = async () => {
64
+ setLoading(true)
65
+ try {
66
+ let response
67
+ if (currentPath === 'public' || currentPath.startsWith('public/')) {
68
+ // Load from public folder API
69
+ const publicPath = currentPath === 'public' ? '' : currentPath.replace('public/', '')
70
+ response = await fetch(`/api/public?folder=${encodeURIComponent(publicPath)}`)
71
+ } else {
72
+ // Load from regular files API
73
+ response = await fetch(`/api/files?folder=${encodeURIComponent(currentPath)}`)
74
+ }
75
+
76
+ const data = await response.json()
77
+
78
+ // Add public folder to root directory
79
+ if (currentPath === '') {
80
+ const publicFolder = {
81
+ name: 'Public Folder',
82
+ type: 'folder' as const,
83
+ path: 'public',
84
+ modified: new Date().toISOString()
85
+ }
86
+
87
+ // Add public folder if it doesn't exist
88
+ if (!data.files.some((f: FileItem) => f.path === 'public')) {
89
+ data.files.unshift(publicFolder)
90
+ }
91
+ }
92
+
93
+ setFiles(data.files || [])
94
+ } catch (error) {
95
+ console.error('Error loading files:', error)
96
+ setFiles([])
97
+ } finally {
98
+ setLoading(false)
99
+ }
100
+ }
101
+
102
+ const handleUpload = async (file: File, targetFolder: string) => {
103
+ const formData = new FormData()
104
+ formData.append('file', file)
105
+
106
+ try {
107
+ let response
108
+ if (isPublicFolder) {
109
+ // Upload to public folder
110
+ const publicPath = currentPath === 'public' ? '' : currentPath.replace('public/', '')
111
+ formData.append('folder', publicPath)
112
+ formData.append('uploadedBy', 'User') // You can customize this
113
+ response = await fetch('/api/public', {
114
+ method: 'POST',
115
+ body: formData
116
+ })
117
+ } else {
118
+ // Upload to regular folder
119
+ formData.append('folder', targetFolder)
120
+ response = await fetch('/api/upload', {
121
+ method: 'POST',
122
+ body: formData
123
+ })
124
+ }
125
+
126
+ const result = await response.json()
127
+ if (result.success) {
128
+ loadFiles() // Reload files
129
+ setUploadModalOpen(false)
130
+ } else {
131
+ alert(`Upload failed: ${result.error}`)
132
+ }
133
+ } catch (error) {
134
+ console.error('Error uploading file:', error)
135
+ alert('Failed to upload file')
136
+ }
137
+ }
138
+
139
+ const handleDownload = (file: FileItem) => {
140
+ window.open(`/api/download?path=${encodeURIComponent(file.path)}`, '_blank')
141
+ }
142
+
143
+ const handlePreview = (file: FileItem) => {
144
+ setPreviewFile(file)
145
+ }
146
+
147
+ const handleDelete = async (file: FileItem) => {
148
+ if (!confirm(`Delete ${file.name}?`)) return
149
+
150
+ try {
151
+ const response = await fetch(`/api/files?path=${encodeURIComponent(file.path)}`, {
152
+ method: 'DELETE'
153
+ })
154
+
155
+ const result = await response.json()
156
+ if (result.success) {
157
+ loadFiles()
158
+ } else {
159
+ alert(`Delete failed: ${result.error}`)
160
+ }
161
+ } catch (error) {
162
+ console.error('Error deleting file:', error)
163
+ alert('Failed to delete file')
164
+ }
165
+ }
166
+
167
+ const handleCreateFolder = async () => {
168
+ const folderName = prompt('Enter folder name:')
169
+ if (!folderName) return
170
+
171
+ try {
172
+ const response = await fetch('/api/files', {
173
+ method: 'POST',
174
+ headers: { 'Content-Type': 'application/json' },
175
+ body: JSON.stringify({
176
+ folderName,
177
+ parentPath: currentPath
178
+ })
179
+ })
180
+
181
+ const result = await response.json()
182
+ if (result.success) {
183
+ loadFiles()
184
+ } else {
185
+ alert(`Create folder failed: ${result.error}`)
186
+ }
187
+ } catch (error) {
188
+ console.error('Error creating folder:', error)
189
+ alert('Failed to create folder')
190
+ }
191
+ }
192
+
193
+ const getFileIcon = (file: FileItem) => {
194
+ if (file.type === 'folder') {
195
+ // Special icon for public folder
196
+ if (file.path === 'public' || file.name === 'Public Folder') {
197
+ return <Users size={48} weight="fill" className="text-purple-400" />
198
+ }
199
+ return <FolderIcon size={48} weight="fill" className="text-orange-400" />
200
+ }
201
+
202
+ const ext = file.extension?.toLowerCase()
203
+ switch (ext) {
204
+ case 'pdf':
205
+ return <FilePdf size={48} weight="fill" className="text-red-500" />
206
+ case 'doc':
207
+ case 'docx':
208
+ return <FileDoc size={48} weight="fill" className="text-blue-500" />
209
+ case 'txt':
210
+ case 'md':
211
+ return <FileText size={48} weight="fill" className="text-gray-600" />
212
+ case 'jpg':
213
+ case 'jpeg':
214
+ case 'png':
215
+ case 'gif':
216
+ return <ImageIcon size={48} weight="fill" className="text-green-500" />
217
+ default:
218
+ return <File size={48} weight="regular" className="text-gray-500" />
219
+ }
220
+ }
221
+
222
+ const formatFileSize = (bytes?: number) => {
223
+ if (!bytes) return ''
224
+ const units = ['B', 'KB', 'MB', 'GB']
225
+ let size = bytes
226
+ let unitIndex = 0
227
+ while (size >= 1024 && unitIndex < units.length - 1) {
228
+ size /= 1024
229
+ unitIndex++
230
+ }
231
+ return `${size.toFixed(1)} ${units[unitIndex]}`
232
+ }
233
+
234
+ const filteredFiles = files.filter(file =>
235
+ file.name.toLowerCase().includes(searchQuery.toLowerCase())
236
+ )
237
+
238
+ // Group files by current directory level
239
+ const currentLevelFiles = filteredFiles.filter(file => {
240
+ const relativePath = currentPath ? file.path.replace(currentPath + '/', '') : file.path
241
+ return !relativePath.includes('/')
242
+ })
243
+
244
+ const handleMouseDown = (e: React.MouseEvent) => {
245
+ if ((e.target as HTMLElement).closest('.window-controls')) return
246
+ setIsDragging(true)
247
+ setDragStart({ x: e.clientX - windowPos.x, y: e.clientY - windowPos.y })
248
+ }
249
+
250
+ const handleMouseMove = (e: MouseEvent) => {
251
+ if (isDragging) {
252
+ setWindowPos({
253
+ x: e.clientX - dragStart.x,
254
+ y: e.clientY - dragStart.y,
255
+ })
256
+ }
257
+ }
258
+
259
+ const handleMouseUp = () => {
260
+ setIsDragging(false)
261
+ }
262
+
263
+ useEffect(() => {
264
+ if (isDragging) {
265
+ window.addEventListener('mousemove', handleMouseMove)
266
+ window.addEventListener('mouseup', handleMouseUp)
267
+ return () => {
268
+ window.removeEventListener('mousemove', handleMouseMove)
269
+ window.removeEventListener('mouseup', handleMouseUp)
270
+ }
271
+ }
272
+ }, [isDragging, dragStart])
273
+
274
+ return (
275
+ <>
276
+ <motion.div
277
+ style={{ left: windowPos.x, top: windowPos.y }}
278
+ className="fixed w-[800px] h-[600px] bg-white rounded-lg shadow-2xl overflow-hidden flex flex-col z-30 select-none"
279
+ >
280
+ {/* Title Bar */}
281
+ <div
282
+ onMouseDown={handleMouseDown}
283
+ className="h-11 bg-gradient-to-b from-[#f6f5f4] to-[#edebe9] border-b border-[#d0d0d0] flex items-center justify-between px-3 cursor-move"
284
+ >
285
+ <div className="flex items-center gap-2 flex-1">
286
+ <div className="flex items-center gap-1 window-controls">
287
+ <button
288
+ onClick={onClose}
289
+ className="w-5 h-5 rounded-full bg-[#E95420] hover:bg-[#d14818] flex items-center justify-center group"
290
+ >
291
+ <X size={12} weight="bold" className="text-white opacity-0 group-hover:opacity-100" />
292
+ </button>
293
+ <button className="w-5 h-5 rounded-full bg-[#ddd] hover:bg-[#ccc] flex items-center justify-center group">
294
+ <Minus size={12} weight="bold" className="text-[#666] opacity-0 group-hover:opacity-100" />
295
+ </button>
296
+ <button className="w-5 h-5 rounded-full bg-[#ddd] hover:bg-[#ccc] flex items-center justify-center group">
297
+ <Square size={10} weight="bold" className="text-[#666] opacity-0 group-hover:opacity-100" />
298
+ </button>
299
+ </div>
300
+ <span className="text-sm font-medium text-[#2c2c2c] ml-2 flex items-center gap-2">
301
+ File Manager - {isPublicFolder ? (
302
+ <>
303
+ <Globe size={16} className="text-purple-500" />
304
+ Public Folder {currentPath.replace('public', '').replace('/', '')}
305
+ </>
306
+ ) : (currentPath || 'Documents')}
307
+ </span>
308
+ </div>
309
+ </div>
310
+
311
+ {/* Toolbar */}
312
+ <div className="h-12 bg-[#fafafa] border-b border-[#e0e0e0] flex items-center px-3 gap-2">
313
+ <div className="flex items-center gap-1">
314
+ <button
315
+ onClick={() => {
316
+ const parent = currentPath.split('/').slice(0, -1).join('/')
317
+ onNavigate(parent)
318
+ }}
319
+ disabled={!currentPath}
320
+ className="p-1.5 hover:bg-[#e8e8e8] rounded disabled:opacity-40 disabled:hover:bg-transparent"
321
+ >
322
+ <CaretLeft size={16} weight="bold" />
323
+ </button>
324
+ <button
325
+ onClick={() => onNavigate('')}
326
+ className="p-1.5 hover:bg-[#e8e8e8] rounded"
327
+ >
328
+ <House size={16} weight="regular" />
329
+ </button>
330
+ </div>
331
+
332
+ <div className="flex items-center gap-1 px-2 py-1.5 bg-white border border-[#ddd] rounded flex-1">
333
+ <House size={14} weight="regular" className="text-[#666]" />
334
+ <span className="text-xs text-[#666]">/</span>
335
+ <span className="text-xs text-[#2c2c2c]">data/documents/{currentPath}</span>
336
+ </div>
337
+
338
+ <div className="flex items-center gap-1">
339
+ <button
340
+ onClick={handleCreateFolder}
341
+ className="p-1.5 hover:bg-[#e8e8e8] rounded"
342
+ title="New Folder"
343
+ >
344
+ <Plus size={16} weight="bold" />
345
+ </button>
346
+ <button
347
+ onClick={() => setUploadModalOpen(true)}
348
+ className="p-1.5 hover:bg-[#e8e8e8] rounded"
349
+ title="Upload File"
350
+ >
351
+ <Upload size={16} weight="regular" />
352
+ </button>
353
+ <div className="flex items-center gap-1 px-2 py-1 bg-white border border-[#ddd] rounded">
354
+ <MagnifyingGlass size={14} weight="regular" className="text-[#666]" />
355
+ <input
356
+ type="text"
357
+ placeholder="Search..."
358
+ value={searchQuery}
359
+ onChange={(e) => setSearchQuery(e.target.value)}
360
+ className="text-xs outline-none w-32"
361
+ />
362
+ </div>
363
+ </div>
364
+ </div>
365
+
366
+ {/* File List */}
367
+ <div className="flex-1 overflow-auto p-4">
368
+ {loading ? (
369
+ <div className="flex items-center justify-center h-full text-[#999] text-sm">
370
+ Loading files...
371
+ </div>
372
+ ) : currentLevelFiles.length === 0 ? (
373
+ <div className="flex items-center justify-center h-full text-[#999] text-sm">
374
+ {searchQuery ? 'No files found' : 'Folder is empty'}
375
+ </div>
376
+ ) : (
377
+ <div className="grid grid-cols-6 gap-4">
378
+ {currentLevelFiles.map((file) => (
379
+ <div
380
+ key={file.path}
381
+ className="group relative"
382
+ >
383
+ <button
384
+ onClick={() => {
385
+ if (file.type === 'folder') {
386
+ onNavigate(file.path)
387
+ } else {
388
+ handlePreview(file)
389
+ }
390
+ }}
391
+ className="flex flex-col items-center gap-2 p-2 hover:bg-[#f0f0f0] rounded w-full"
392
+ >
393
+ <div className="w-16 h-16 flex items-center justify-center">
394
+ {getFileIcon(file)}
395
+ </div>
396
+ <span className="text-xs text-[#2c2c2c] text-center break-all w-full line-clamp-2">
397
+ {file.name}
398
+ </span>
399
+ {file.size && (
400
+ <span className="text-xs text-[#666]">
401
+ {formatFileSize(file.size)}
402
+ </span>
403
+ )}
404
+ </button>
405
+
406
+ {/* File Actions */}
407
+ {file.type === 'file' && (
408
+ <div className="absolute top-1 right-1 hidden group-hover:flex gap-1">
409
+ <button
410
+ onClick={(e) => {
411
+ e.stopPropagation()
412
+ handlePreview(file)
413
+ }}
414
+ className="p-1 bg-white rounded shadow hover:bg-[#f0f0f0]"
415
+ title="Preview"
416
+ >
417
+ <Eye size={14} />
418
+ </button>
419
+ <button
420
+ onClick={(e) => {
421
+ e.stopPropagation()
422
+ handleDownload(file)
423
+ }}
424
+ className="p-1 bg-white rounded shadow hover:bg-[#f0f0f0]"
425
+ title="Download"
426
+ >
427
+ <Download size={14} />
428
+ </button>
429
+ <button
430
+ onClick={(e) => {
431
+ e.stopPropagation()
432
+ handleDelete(file)
433
+ }}
434
+ className="p-1 bg-white rounded shadow hover:bg-red-100"
435
+ title="Delete"
436
+ >
437
+ <Trash size={14} className="text-red-600" />
438
+ </button>
439
+ </div>
440
+ )}
441
+ </div>
442
+ ))}
443
+ </div>
444
+ )}
445
+ </div>
446
+
447
+ {/* Status Bar */}
448
+ <div className="h-6 bg-[#f6f5f4] border-t border-[#d0d0d0] flex items-center px-3 text-xs text-[#666]">
449
+ {currentLevelFiles.length} items • {files.filter(f => f.type === 'folder').length} folders • {files.filter(f => f.type === 'file').length} files
450
+ </div>
451
+ </motion.div>
452
+
453
+ {/* Upload Modal */}
454
+ {uploadModalOpen && (
455
+ <UploadModal
456
+ currentPath={currentPath}
457
+ onUpload={handleUpload}
458
+ onClose={() => setUploadModalOpen(false)}
459
+ />
460
+ )}
461
+
462
+ {/* File Preview Modal */}
463
+ {previewFile && (
464
+ <FilePreview
465
+ file={previewFile}
466
+ onClose={() => setPreviewFile(null)}
467
+ onDownload={() => handleDownload(previewFile)}
468
+ />
469
+ )}
470
+ </>
471
+ )
472
+ }
473
+
474
+ // Upload Modal Component
475
+ function UploadModal({
476
+ currentPath,
477
+ onUpload,
478
+ onClose
479
+ }: {
480
+ currentPath: string
481
+ onUpload: (file: File, folder: string) => void
482
+ onClose: () => void
483
+ }) {
484
+ const [selectedFile, setSelectedFile] = useState<File | null>(null)
485
+ const [targetFolder, setTargetFolder] = useState(currentPath)
486
+
487
+ const handleSubmit = () => {
488
+ if (selectedFile) {
489
+ onUpload(selectedFile, targetFolder)
490
+ }
491
+ }
492
+
493
+ return (
494
+ <div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50">
495
+ <div className="bg-white rounded-lg p-6 w-[400px]">
496
+ <h2 className="text-lg font-semibold mb-4">Upload File</h2>
497
+
498
+ <div className="space-y-4">
499
+ <div>
500
+ <label className="block text-sm font-medium mb-2">Select File</label>
501
+ <input
502
+ type="file"
503
+ onChange={(e) => setSelectedFile(e.target.files?.[0] || null)}
504
+ className="w-full p-2 border rounded"
505
+ />
506
+ </div>
507
+
508
+ <div>
509
+ <label className="block text-sm font-medium mb-2">Upload to Folder</label>
510
+ <input
511
+ type="text"
512
+ value={targetFolder}
513
+ onChange={(e) => setTargetFolder(e.target.value)}
514
+ placeholder="e.g., homework/math"
515
+ className="w-full p-2 border rounded"
516
+ />
517
+ <p className="text-xs text-gray-500 mt-1">
518
+ Leave empty for root, or enter path like "folder/subfolder"
519
+ </p>
520
+ </div>
521
+
522
+ <div className="flex justify-end gap-2">
523
+ <button
524
+ onClick={onClose}
525
+ className="px-4 py-2 text-gray-600 hover:bg-gray-100 rounded"
526
+ >
527
+ Cancel
528
+ </button>
529
+ <button
530
+ onClick={handleSubmit}
531
+ disabled={!selectedFile}
532
+ className="px-4 py-2 bg-[#E95420] text-white rounded hover:bg-[#d14818] disabled:opacity-50"
533
+ >
534
+ Upload
535
+ </button>
536
+ </div>
537
+ </div>
538
+ </div>
539
+ </div>
540
+ )
541
+ }
542
+
app/components/FilePreview.tsx ADDED
@@ -0,0 +1,372 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ 'use client'
2
+
3
+ import React, { useState, useEffect } from 'react'
4
+ import { X, Download, MagnifyingGlassPlus, MagnifyingGlassMinus } from '@phosphor-icons/react'
5
+ import mammoth from 'mammoth'
6
+ import * as XLSX from 'xlsx'
7
+ import { PDFViewer } from './PDFViewer'
8
+
9
+ interface FilePreviewProps {
10
+ file: {
11
+ name: string
12
+ path: string
13
+ extension?: string
14
+ size?: number
15
+ }
16
+ onClose: () => void
17
+ onDownload: () => void
18
+ }
19
+
20
+ export function FilePreview({ file, onClose, onDownload }: FilePreviewProps) {
21
+ const [content, setContent] = useState<any>(null)
22
+ const [loading, setLoading] = useState(true)
23
+ const [error, setError] = useState<string | null>(null)
24
+ const [scale, setScale] = useState(1)
25
+
26
+ // Excel specific state
27
+ const [activeSheet, setActiveSheet] = useState(0)
28
+
29
+ const previewUrl = `/api/download?path=${encodeURIComponent(file.path)}&preview=true`
30
+ const ext = file.extension?.toLowerCase() || ''
31
+
32
+ useEffect(() => {
33
+ loadFileContent()
34
+ }, [file])
35
+
36
+ const loadFileContent = async () => {
37
+ setLoading(true)
38
+ setError(null)
39
+
40
+ try {
41
+ switch (ext) {
42
+ case 'docx':
43
+ case 'doc':
44
+ await loadWordDocument()
45
+ break
46
+ case 'xlsx':
47
+ case 'xls':
48
+ await loadExcelDocument()
49
+ break
50
+ case 'pptx':
51
+ case 'ppt':
52
+ await loadPowerPointDocument()
53
+ break
54
+ case 'txt':
55
+ case 'md':
56
+ case 'json':
57
+ case 'js':
58
+ case 'ts':
59
+ case 'jsx':
60
+ case 'tsx':
61
+ case 'css':
62
+ case 'html':
63
+ case 'xml':
64
+ case 'py':
65
+ case 'java':
66
+ case 'cpp':
67
+ case 'c':
68
+ case 'h':
69
+ case 'sh':
70
+ case 'yaml':
71
+ case 'yml':
72
+ await loadTextFile()
73
+ break
74
+ case 'csv':
75
+ await loadCSVFile()
76
+ break
77
+ default:
78
+ setContent(null)
79
+ }
80
+ } catch (err) {
81
+ setError(err instanceof Error ? err.message : 'Failed to load file')
82
+ } finally {
83
+ setLoading(false)
84
+ }
85
+ }
86
+
87
+ const loadWordDocument = async () => {
88
+ try {
89
+ const response = await fetch(previewUrl)
90
+ const arrayBuffer = await response.arrayBuffer()
91
+ const result = await mammoth.convertToHtml({ arrayBuffer })
92
+ setContent({ type: 'html', data: result.value })
93
+ } catch (err) {
94
+ throw new Error('Failed to load Word document')
95
+ }
96
+ }
97
+
98
+ const loadExcelDocument = async () => {
99
+ try {
100
+ const response = await fetch(previewUrl)
101
+ const arrayBuffer = await response.arrayBuffer()
102
+ const workbook = XLSX.read(arrayBuffer, { type: 'array' })
103
+
104
+ // Convert all sheets to HTML
105
+ const sheets = workbook.SheetNames.map(name => ({
106
+ name,
107
+ html: XLSX.utils.sheet_to_html(workbook.Sheets[name], {
108
+ header: '<table class="excel-table">',
109
+ footer: '</table>'
110
+ })
111
+ }))
112
+
113
+ setContent({ type: 'excel', data: sheets })
114
+ } catch (err) {
115
+ throw new Error('Failed to load Excel document')
116
+ }
117
+ }
118
+
119
+ const loadPowerPointDocument = async () => {
120
+ // For PowerPoint, we'll show a message to download
121
+ // Full PowerPoint rendering requires more complex libraries
122
+ setContent({ type: 'powerpoint', data: null })
123
+ }
124
+
125
+ const loadTextFile = async () => {
126
+ try {
127
+ const response = await fetch(previewUrl)
128
+ const text = await response.text()
129
+ setContent({ type: 'text', data: text })
130
+ } catch (err) {
131
+ throw new Error('Failed to load text file')
132
+ }
133
+ }
134
+
135
+ const loadCSVFile = async () => {
136
+ try {
137
+ const response = await fetch(previewUrl)
138
+ const text = await response.text()
139
+ const workbook = XLSX.read(text, { type: 'string' })
140
+ const sheet = workbook.Sheets[workbook.SheetNames[0]]
141
+ const html = XLSX.utils.sheet_to_html(sheet, {
142
+ header: '<table class="csv-table">',
143
+ footer: '</table>'
144
+ })
145
+ setContent({ type: 'html', data: html })
146
+ } catch (err) {
147
+ throw new Error('Failed to load CSV file')
148
+ }
149
+ }
150
+
151
+
152
+ const renderContent = () => {
153
+ if (loading) {
154
+ return (
155
+ <div className="flex items-center justify-center h-full">
156
+ <div className="text-gray-600">Loading...</div>
157
+ </div>
158
+ )
159
+ }
160
+
161
+ if (error) {
162
+ return (
163
+ <div className="flex flex-col items-center justify-center h-full">
164
+ <p className="text-red-600 mb-4">{error}</p>
165
+ <button
166
+ onClick={onDownload}
167
+ className="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600"
168
+ >
169
+ Download to View
170
+ </button>
171
+ </div>
172
+ )
173
+ }
174
+
175
+ // PDF files
176
+ if (ext === 'pdf') {
177
+ return (
178
+ <PDFViewer
179
+ url={previewUrl}
180
+ scale={scale}
181
+ onZoomIn={() => setScale(prev => Math.min(2, prev + 0.1))}
182
+ onZoomOut={() => setScale(prev => Math.max(0.5, prev - 0.1))}
183
+ />
184
+ )
185
+ }
186
+
187
+ // Image files
188
+ if (['jpg', 'jpeg', 'png', 'gif', 'svg', 'webp', 'bmp'].includes(ext)) {
189
+ return (
190
+ <div className="flex items-center justify-center h-full p-4">
191
+ <img
192
+ src={previewUrl}
193
+ alt={file.name}
194
+ className="max-w-full max-h-full object-contain"
195
+ style={{ transform: `scale(${scale})` }}
196
+ />
197
+ <div className="absolute bottom-4 left-1/2 -translate-x-1/2 flex items-center gap-2 bg-white rounded-lg shadow-lg p-2">
198
+ <button
199
+ onClick={() => setScale(prev => Math.max(0.5, prev - 0.1))}
200
+ className="p-1 rounded hover:bg-gray-100"
201
+ >
202
+ <MagnifyingGlassMinus size={16} />
203
+ </button>
204
+ <span className="text-sm px-2">{Math.round(scale * 100)}%</span>
205
+ <button
206
+ onClick={() => setScale(prev => Math.min(3, prev + 0.1))}
207
+ className="p-1 rounded hover:bg-gray-100"
208
+ >
209
+ <MagnifyingGlassPlus size={16} />
210
+ </button>
211
+ </div>
212
+ </div>
213
+ )
214
+ }
215
+
216
+ // HTML content (Word docs, converted HTML)
217
+ if (content?.type === 'html') {
218
+ return (
219
+ <div className="p-4 overflow-auto h-full">
220
+ <div
221
+ dangerouslySetInnerHTML={{ __html: content.data }}
222
+ className="prose max-w-none"
223
+ />
224
+ </div>
225
+ )
226
+ }
227
+
228
+ // Excel sheets
229
+ if (content?.type === 'excel') {
230
+ return (
231
+ <div className="flex flex-col h-full">
232
+ <div className="flex gap-2 p-2 border-b">
233
+ {content.data.map((sheet: any, index: number) => (
234
+ <button
235
+ key={index}
236
+ onClick={() => setActiveSheet(index)}
237
+ className={`px-3 py-1 rounded ${
238
+ activeSheet === index ? 'bg-blue-500 text-white' : 'bg-gray-100'
239
+ }`}
240
+ >
241
+ {sheet.name}
242
+ </button>
243
+ ))}
244
+ </div>
245
+ <div className="flex-1 overflow-auto p-4">
246
+ <div
247
+ dangerouslySetInnerHTML={{ __html: content.data[activeSheet].html }}
248
+ className="excel-preview"
249
+ />
250
+ </div>
251
+ </div>
252
+ )
253
+ }
254
+
255
+ // Text files
256
+ if (content?.type === 'text') {
257
+ return (
258
+ <div className="h-full overflow-auto">
259
+ <pre className="p-4 text-sm font-mono whitespace-pre-wrap">
260
+ {content.data}
261
+ </pre>
262
+ </div>
263
+ )
264
+ }
265
+
266
+ // PowerPoint placeholder
267
+ if (content?.type === 'powerpoint') {
268
+ return (
269
+ <div className="flex flex-col items-center justify-center h-full">
270
+ <div className="text-6xl mb-4">📊</div>
271
+ <p className="text-gray-600 mb-4">PowerPoint presentation</p>
272
+ <p className="text-sm text-gray-500 mb-4">Preview requires download</p>
273
+ <button
274
+ onClick={onDownload}
275
+ className="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600"
276
+ >
277
+ Download to View
278
+ </button>
279
+ </div>
280
+ )
281
+ }
282
+
283
+ // Default fallback
284
+ return (
285
+ <iframe
286
+ src={previewUrl}
287
+ className="w-full h-full border-0"
288
+ title={file.name}
289
+ />
290
+ )
291
+ }
292
+
293
+ return (
294
+ <div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50">
295
+ <div className="bg-white rounded-lg w-[90%] h-[90%] flex flex-col">
296
+ <div className="flex items-center justify-between p-4 border-b">
297
+ <div>
298
+ <h2 className="text-lg font-semibold">{file.name}</h2>
299
+ <p className="text-sm text-gray-500">
300
+ {ext.toUpperCase()} • {file.size ? formatFileSize(file.size) : 'Unknown size'}
301
+ </p>
302
+ </div>
303
+ <div className="flex gap-2">
304
+ <button
305
+ onClick={onDownload}
306
+ className="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600 flex items-center gap-2"
307
+ >
308
+ <Download size={20} />
309
+ Download
310
+ </button>
311
+ <button
312
+ onClick={onClose}
313
+ className="p-2 hover:bg-gray-100 rounded"
314
+ >
315
+ <X size={20} />
316
+ </button>
317
+ </div>
318
+ </div>
319
+
320
+ <div className="flex-1 overflow-hidden relative">
321
+ {renderContent()}
322
+ </div>
323
+ </div>
324
+
325
+ <style jsx global>{`
326
+ .excel-table, .csv-table {
327
+ border-collapse: collapse;
328
+ width: 100%;
329
+ }
330
+ .excel-table td, .excel-table th,
331
+ .csv-table td, .csv-table th {
332
+ border: 1px solid #ddd;
333
+ padding: 8px;
334
+ text-align: left;
335
+ }
336
+ .excel-table th, .csv-table th {
337
+ background-color: #f2f2f2;
338
+ font-weight: bold;
339
+ }
340
+ .excel-table tr:nth-child(even),
341
+ .csv-table tr:nth-child(even) {
342
+ background-color: #f9f9f9;
343
+ }
344
+ .excel-preview {
345
+ font-family: 'Arial', sans-serif;
346
+ }
347
+ .prose {
348
+ max-width: none;
349
+ }
350
+ .prose h1 { font-size: 2em; font-weight: bold; margin: 1em 0 0.5em; }
351
+ .prose h2 { font-size: 1.5em; font-weight: bold; margin: 1em 0 0.5em; }
352
+ .prose h3 { font-size: 1.2em; font-weight: bold; margin: 1em 0 0.5em; }
353
+ .prose p { margin: 1em 0; }
354
+ .prose ul, .prose ol { margin: 1em 0; padding-left: 2em; }
355
+ .prose li { margin: 0.5em 0; }
356
+ .prose strong { font-weight: bold; }
357
+ .prose em { font-style: italic; }
358
+ `}</style>
359
+ </div>
360
+ )
361
+ }
362
+
363
+ function formatFileSize(bytes: number): string {
364
+ const units = ['B', 'KB', 'MB', 'GB']
365
+ let size = bytes
366
+ let unitIndex = 0
367
+ while (size >= 1024 && unitIndex < units.length - 1) {
368
+ size /= 1024
369
+ unitIndex++
370
+ }
371
+ return `${size.toFixed(1)} ${units[unitIndex]}`
372
+ }
app/components/GeminiChat.tsx ADDED
@@ -0,0 +1,258 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ 'use client'
2
+
3
+ import React, { useState, useRef, useEffect } from 'react'
4
+ import Window from './Window'
5
+ import {
6
+ PaperPlaneRight,
7
+ Sparkle,
8
+ ArrowUp
9
+ } from '@phosphor-icons/react'
10
+
11
+ interface GeminiChatProps {
12
+ onClose: () => void
13
+ }
14
+
15
+ interface Message {
16
+ id: string
17
+ role: 'user' | 'assistant'
18
+ content: string
19
+ timestamp: number
20
+ }
21
+
22
+ export function GeminiChat({ onClose }: GeminiChatProps) {
23
+ const [messages, setMessages] = useState<Message[]>([
24
+ {
25
+ id: '1',
26
+ role: 'assistant',
27
+ content: "Hello! I'm Gemini. How can I help you today?",
28
+ timestamp: Date.now()
29
+ }
30
+ ])
31
+ const [input, setInput] = useState('')
32
+ const [isLoading, setIsLoading] = useState(false)
33
+ const scrollRef = useRef<HTMLDivElement>(null)
34
+ const inputRef = useRef<HTMLInputElement>(null)
35
+
36
+ useEffect(() => {
37
+ // Load messages from localStorage
38
+ const savedMessages = localStorage.getItem('gemini-chat-messages')
39
+ if (savedMessages) {
40
+ try {
41
+ const parsed = JSON.parse(savedMessages)
42
+ if (parsed.length > 0) {
43
+ setMessages(parsed)
44
+ }
45
+ } catch (e) {
46
+ console.error('Failed to load messages')
47
+ }
48
+ }
49
+ }, [])
50
+
51
+ useEffect(() => {
52
+ // Save messages to localStorage
53
+ if (messages.length > 1) {
54
+ localStorage.setItem('gemini-chat-messages', JSON.stringify(messages.slice(-20))) // Keep last 20 messages
55
+ }
56
+ }, [messages])
57
+
58
+ useEffect(() => {
59
+ if (scrollRef.current) {
60
+ scrollRef.current.scrollTop = scrollRef.current.scrollHeight
61
+ }
62
+ }, [messages])
63
+
64
+ const handleSend = async () => {
65
+ if (!input.trim() || isLoading) return
66
+
67
+ const userMessage: Message = {
68
+ id: Date.now().toString(),
69
+ role: 'user',
70
+ content: input.trim(),
71
+ timestamp: Date.now()
72
+ }
73
+
74
+ setMessages(prev => [...prev, userMessage])
75
+ setInput('')
76
+ setIsLoading(true)
77
+
78
+ try {
79
+ // Get conversation history (last 10 messages)
80
+ const history = messages.slice(-10).map(msg => ({
81
+ role: msg.role,
82
+ content: msg.content
83
+ }))
84
+
85
+ const response = await fetch('/api/gemini/chat', {
86
+ method: 'POST',
87
+ headers: {
88
+ 'Content-Type': 'application/json',
89
+ },
90
+ body: JSON.stringify({
91
+ message: userMessage.content,
92
+ history
93
+ }),
94
+ })
95
+
96
+ const data = await response.json()
97
+
98
+ if (response.ok && data.response) {
99
+ const assistantMessage: Message = {
100
+ id: (Date.now() + 1).toString(),
101
+ role: 'assistant',
102
+ content: data.response,
103
+ timestamp: Date.now()
104
+ }
105
+ setMessages(prev => [...prev, assistantMessage])
106
+ } else {
107
+ // Fallback response if API fails
108
+ const fallbackResponses = [
109
+ "I'm here to help! What would you like to know?",
110
+ "That's interesting! Tell me more about it.",
111
+ "I can help with that! Let me think about the best approach.",
112
+ "Great question! Here's what I think...",
113
+ "I understand. Let me help you with that."
114
+ ]
115
+ const randomResponse = fallbackResponses[Math.floor(Math.random() * fallbackResponses.length)]
116
+
117
+ const assistantMessage: Message = {
118
+ id: (Date.now() + 1).toString(),
119
+ role: 'assistant',
120
+ content: randomResponse,
121
+ timestamp: Date.now()
122
+ }
123
+ setMessages(prev => [...prev, assistantMessage])
124
+ }
125
+ } catch (error) {
126
+ console.error('Error sending message:', error)
127
+ const errorMessage: Message = {
128
+ id: (Date.now() + 1).toString(),
129
+ role: 'assistant',
130
+ content: "I'm currently running in demo mode. For full functionality, please ensure the API is configured.",
131
+ timestamp: Date.now()
132
+ }
133
+ setMessages(prev => [...prev, errorMessage])
134
+ }
135
+
136
+ setIsLoading(false)
137
+ }
138
+
139
+ const handleKeyPress = (e: React.KeyboardEvent) => {
140
+ if (e.key === 'Enter' && !e.shiftKey) {
141
+ e.preventDefault()
142
+ handleSend()
143
+ }
144
+ }
145
+
146
+ const clearChat = () => {
147
+ setMessages([
148
+ {
149
+ id: Date.now().toString(),
150
+ role: 'assistant',
151
+ content: "Chat history cleared. How can I help you today?",
152
+ timestamp: Date.now()
153
+ }
154
+ ])
155
+ localStorage.removeItem('gemini-chat-messages')
156
+ }
157
+
158
+ return (
159
+ <Window
160
+ id="gemini"
161
+ title="Gemini Ultra"
162
+ isOpen={true}
163
+ onClose={onClose}
164
+ width={700}
165
+ height={500}
166
+ x={100}
167
+ y={100}
168
+ className="gemini-window"
169
+ headerClassName="bg-white border-b border-gray-100"
170
+ >
171
+ <div className="flex flex-col h-full bg-white">
172
+ {/* Chat Header */}
173
+ <div className="px-4 py-2 border-b border-gray-100 flex items-center justify-between">
174
+ <div className="flex items-center gap-2">
175
+ <div className="w-8 h-8 rounded-full bg-gradient-to-br from-blue-500 to-pink-600 flex items-center justify-center">
176
+ <Sparkle size={18} weight="fill" className="text-white" />
177
+ </div>
178
+ <span className="text-transparent bg-clip-text bg-gradient-to-r from-blue-500 to-pink-600 font-bold">
179
+ Gemini Ultra
180
+ </span>
181
+ </div>
182
+ <button
183
+ onClick={clearChat}
184
+ className="text-xs text-gray-500 hover:text-gray-700"
185
+ >
186
+ Clear chat
187
+ </button>
188
+ </div>
189
+
190
+ {/* Messages Area */}
191
+ <div
192
+ ref={scrollRef}
193
+ className="flex-1 overflow-y-auto p-4 space-y-4"
194
+ >
195
+ {messages.map(message => (
196
+ <div
197
+ key={message.id}
198
+ className={`flex ${message.role === 'user' ? 'justify-end' : 'justify-start'}`}
199
+ >
200
+ <div
201
+ className={`max-w-[80%] ${
202
+ message.role === 'user'
203
+ ? 'bg-blue-600 text-white rounded-2xl rounded-tr-none'
204
+ : 'bg-gray-100 text-gray-800 rounded-2xl rounded-tl-none'
205
+ } px-4 py-2 text-sm`}
206
+ >
207
+ {message.role === 'assistant' && (
208
+ <p className="font-semibold text-blue-600 mb-1 text-xs">Gemini</p>
209
+ )}
210
+ <p className="whitespace-pre-wrap">{message.content}</p>
211
+ </div>
212
+ </div>
213
+ ))}
214
+
215
+ {isLoading && (
216
+ <div className="flex justify-start">
217
+ <div className="bg-gray-100 rounded-2xl rounded-tl-none px-4 py-3 text-sm">
218
+ <p className="font-semibold text-blue-600 mb-1 text-xs">Gemini</p>
219
+ <div className="flex gap-1">
220
+ <span className="w-2 h-2 bg-gray-400 rounded-full animate-bounce" style={{ animationDelay: '0ms' }}></span>
221
+ <span className="w-2 h-2 bg-gray-400 rounded-full animate-bounce" style={{ animationDelay: '150ms' }}></span>
222
+ <span className="w-2 h-2 bg-gray-400 rounded-full animate-bounce" style={{ animationDelay: '300ms' }}></span>
223
+ </div>
224
+ </div>
225
+ </div>
226
+ )}
227
+ </div>
228
+
229
+ {/* Input Area */}
230
+ <div className="border-t border-gray-100 p-4">
231
+ <div className="flex items-center gap-2">
232
+ <input
233
+ ref={inputRef}
234
+ type="text"
235
+ value={input}
236
+ onChange={(e) => setInput(e.target.value)}
237
+ onKeyDown={handleKeyPress}
238
+ placeholder="Ask Gemini..."
239
+ className="flex-1 bg-gray-100 rounded-full px-4 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-200 transition-all"
240
+ disabled={isLoading}
241
+ />
242
+ <button
243
+ onClick={handleSend}
244
+ disabled={!input.trim() || isLoading}
245
+ className={`w-8 h-8 rounded-full flex items-center justify-center transition-colors ${
246
+ input.trim() && !isLoading
247
+ ? 'bg-blue-600 hover:bg-blue-700 text-white'
248
+ : 'bg-gray-200 text-gray-400 cursor-not-allowed'
249
+ }`}
250
+ >
251
+ <ArrowUp size={16} weight="bold" />
252
+ </button>
253
+ </div>
254
+ </div>
255
+ </div>
256
+ </Window>
257
+ )
258
+ }
app/components/HelpModal.tsx ADDED
@@ -0,0 +1,172 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ 'use client'
2
+
3
+ import React, { useState } from 'react'
4
+ import { motion, AnimatePresence } from 'framer-motion'
5
+ import { X, Minus, Square, Info, UserCircle, Target } from '@phosphor-icons/react'
6
+
7
+ interface HelpModalProps {
8
+ isOpen: boolean
9
+ onClose: () => void
10
+ }
11
+
12
+ export function HelpModal({ isOpen, onClose }: HelpModalProps) {
13
+ const [windowPos, setWindowPos] = useState({ x: 100, y: 100 })
14
+ const [isDragging, setIsDragging] = useState(false)
15
+ const [dragStart, setDragStart] = useState({ x: 0, y: 0 })
16
+
17
+ const handleMouseDown = (e: React.MouseEvent) => {
18
+ if ((e.target as HTMLElement).closest('.window-controls')) return
19
+ setIsDragging(true)
20
+ setDragStart({ x: e.clientX - windowPos.x, y: e.clientY - windowPos.y })
21
+ }
22
+
23
+ const handleMouseMove = (e: MouseEvent) => {
24
+ if (isDragging) {
25
+ setWindowPos({
26
+ x: e.clientX - dragStart.x,
27
+ y: e.clientY - dragStart.y,
28
+ })
29
+ }
30
+ }
31
+
32
+ const handleMouseUp = () => {
33
+ setIsDragging(false)
34
+ }
35
+
36
+ React.useEffect(() => {
37
+ if (isDragging) {
38
+ window.addEventListener('mousemove', handleMouseMove)
39
+ window.addEventListener('mouseup', handleMouseUp)
40
+ return () => {
41
+ window.removeEventListener('mousemove', handleMouseMove)
42
+ window.removeEventListener('mouseup', handleMouseUp)
43
+ }
44
+ }
45
+ }, [isDragging, dragStart, windowPos])
46
+
47
+ return (
48
+ <AnimatePresence>
49
+ {isOpen && (
50
+ <>
51
+ {/* Backdrop */}
52
+ <motion.div
53
+ initial={{ opacity: 0 }}
54
+ animate={{ opacity: 1 }}
55
+ exit={{ opacity: 0 }}
56
+ className="fixed inset-0 bg-black/30 backdrop-blur-sm z-50"
57
+ />
58
+
59
+ {/* Modal */}
60
+ <motion.div
61
+ style={{ left: windowPos.x, top: windowPos.y }}
62
+ initial={{ scale: 0.9, opacity: 0 }}
63
+ animate={{ scale: 1, opacity: 1 }}
64
+ exit={{ scale: 0.9, opacity: 0 }}
65
+ transition={{ type: "spring", damping: 20, stiffness: 300 }}
66
+ className="fixed w-[600px] bg-white rounded-lg shadow-2xl z-50 select-none"
67
+ >
68
+ {/* Ubuntu-style Window Header */}
69
+ <div
70
+ onMouseDown={handleMouseDown}
71
+ className="h-11 bg-gradient-to-b from-[#f6f5f4] to-[#edebe9] border-b border-[#d0d0d0] flex items-center justify-between px-3 cursor-move rounded-t-lg"
72
+ >
73
+ <div className="flex items-center gap-2 flex-1">
74
+ <div className="flex items-center gap-1 window-controls">
75
+ <button
76
+ onClick={onClose}
77
+ className="w-5 h-5 rounded-full bg-[#E95420] hover:bg-[#d14818] flex items-center justify-center group"
78
+ >
79
+ <X size={12} weight="bold" className="text-white opacity-0 group-hover:opacity-100" />
80
+ </button>
81
+ <button className="w-5 h-5 rounded-full bg-[#ddd] hover:bg-[#ccc] flex items-center justify-center group">
82
+ <Minus size={12} weight="bold" className="text-[#666] opacity-0 group-hover:opacity-100" />
83
+ </button>
84
+ <button className="w-5 h-5 rounded-full bg-[#ddd] hover:bg-[#ccc] flex items-center justify-center group">
85
+ <Square size={10} weight="bold" className="text-[#666] opacity-0 group-hover:opacity-100" />
86
+ </button>
87
+ </div>
88
+ <div className="flex items-center gap-2 ml-2">
89
+ <Info size={18} weight="fill" className="text-[#E95420]" />
90
+ <span className="text-sm font-medium text-[#2c2c2c]">About This Application</span>
91
+ </div>
92
+ </div>
93
+ </div>
94
+
95
+ {/* Content */}
96
+ <div className="p-6 space-y-5 bg-white rounded-b-lg">
97
+ {/* Creator Info */}
98
+ <div className="flex items-start gap-4 p-4 bg-gradient-to-r from-[#E95420]/10 to-orange-100 rounded-lg border border-[#E95420]/20">
99
+ <div className="w-10 h-10 rounded-full bg-gradient-to-br from-[#E95420] to-[#d14818] flex items-center justify-center flex-shrink-0">
100
+ <UserCircle size={24} weight="fill" className="text-white" />
101
+ </div>
102
+ <div>
103
+ <h3 className="text-base font-semibold text-[#2c2c2c] mb-1">Created By</h3>
104
+ <p className="text-[#555] font-medium">Reuben Chagas Fernandes</p>
105
+ </div>
106
+ </div>
107
+
108
+ {/* Purpose */}
109
+ <div className="flex items-start gap-4 p-4 bg-blue-50 rounded-lg border border-blue-200">
110
+ <div className="w-10 h-10 rounded-full bg-gradient-to-br from-blue-500 to-blue-600 flex items-center justify-center flex-shrink-0">
111
+ <Target size={24} weight="fill" className="text-white" />
112
+ </div>
113
+ <div>
114
+ <h3 className="text-base font-semibold text-[#2c2c2c] mb-2">Purpose</h3>
115
+ <p className="text-[#555] leading-relaxed text-sm">
116
+ This application was created for sharing study material and making it easily
117
+ accessible through Claude. Our goal is to provide a seamless platform for
118
+ students to collaborate, share resources, and enhance their learning experience.
119
+ </p>
120
+ </div>
121
+ </div>
122
+
123
+ {/* Features */}
124
+ <div className="bg-gradient-to-br from-purple-50 to-pink-50 rounded-lg p-4 border border-purple-200">
125
+ <h4 className="text-sm font-semibold text-[#2c2c2c] uppercase tracking-wider mb-3 flex items-center gap-2">
126
+ <span className="w-1 h-4 bg-[#E95420] rounded-full"></span>
127
+ Key Features
128
+ </h4>
129
+ <ul className="space-y-2.5 text-[#555]">
130
+ <li className="flex items-center gap-3">
131
+ <span className="w-2 h-2 bg-blue-500 rounded-full flex-shrink-0" />
132
+ <span className="text-sm">Easy file upload and sharing</span>
133
+ </li>
134
+ <li className="flex items-center gap-3">
135
+ <span className="w-2 h-2 bg-purple-500 rounded-full flex-shrink-0" />
136
+ <span className="text-sm">Integration with Claude AI</span>
137
+ </li>
138
+ <li className="flex items-center gap-3">
139
+ <span className="w-2 h-2 bg-green-500 rounded-full flex-shrink-0" />
140
+ <span className="text-sm">Public folder for community sharing</span>
141
+ </li>
142
+ <li className="flex items-center gap-3">
143
+ <span className="w-2 h-2 bg-[#E95420] rounded-full flex-shrink-0" />
144
+ <span className="text-sm">Exam calendar and organization tools</span>
145
+ </li>
146
+ <li className="flex items-center gap-3">
147
+ <span className="w-2 h-2 bg-cyan-500 rounded-full flex-shrink-0" />
148
+ <span className="text-sm">Web browser with CORS proxy support</span>
149
+ </li>
150
+ <li className="flex items-center gap-3">
151
+ <span className="w-2 h-2 bg-orange-500 rounded-full flex-shrink-0" />
152
+ <span className="text-sm">Gemini AI chat assistant</span>
153
+ </li>
154
+ </ul>
155
+ </div>
156
+
157
+ {/* Footer Button */}
158
+ <div className="pt-2">
159
+ <button
160
+ onClick={onClose}
161
+ className="w-full py-2.5 bg-[#E95420] hover:bg-[#d14818] text-white rounded-lg font-medium transition-colors shadow-sm"
162
+ >
163
+ Close
164
+ </button>
165
+ </div>
166
+ </div>
167
+ </motion.div>
168
+ </>
169
+ )}
170
+ </AnimatePresence>
171
+ )
172
+ }
app/components/MatrixRain.tsx ADDED
@@ -0,0 +1,236 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ 'use client'
2
+
3
+ import React, { useEffect, useRef, useState } from 'react'
4
+ import { motion, AnimatePresence } from 'framer-motion'
5
+
6
+ interface MatrixRainProps {
7
+ isActive: boolean
8
+ onComplete?: () => void
9
+ }
10
+
11
+ export function MatrixRain({ isActive, onComplete }: MatrixRainProps) {
12
+ const canvasRef = useRef<HTMLCanvasElement>(null)
13
+ const [showText, setShowText] = useState(false)
14
+
15
+ useEffect(() => {
16
+ if (!isActive) return
17
+
18
+ const canvas = canvasRef.current
19
+ if (!canvas) return
20
+
21
+ const ctx = canvas.getContext('2d')
22
+ if (!ctx) return
23
+
24
+ // Set canvas size
25
+ canvas.width = window.innerWidth
26
+ canvas.height = window.innerHeight
27
+
28
+ // Matrix rain configuration
29
+ const fontSize = 14
30
+ const columns = Math.floor(canvas.width / fontSize)
31
+ const drops: number[] = []
32
+
33
+ // Initialize drops
34
+ for (let i = 0; i < columns; i++) {
35
+ drops[i] = Math.random() * -100
36
+ }
37
+
38
+ // Characters to use in the rain
39
+ const matrix = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789@#$%^&*()_+-=[]{}|;:<>,.?/~`'
40
+ const matrixArray = matrix.split('')
41
+
42
+ // Special word to display
43
+ const specialWord = 'REUBENOS'
44
+ let specialWordTimer: NodeJS.Timeout
45
+
46
+ // Show the special text after 1.5 seconds
47
+ if (isActive) {
48
+ specialWordTimer = setTimeout(() => {
49
+ setShowText(true)
50
+ }, 1500)
51
+ }
52
+
53
+ // Drawing function
54
+ function draw() {
55
+ if (!ctx || !canvas) return
56
+
57
+ // Semi-transparent black to create fade effect
58
+ ctx.fillStyle = 'rgba(0, 0, 0, 0.05)'
59
+ ctx.fillRect(0, 0, canvas.width, canvas.height)
60
+
61
+ // Green text
62
+ ctx.fillStyle = '#0F0'
63
+ ctx.font = `${fontSize}px monospace`
64
+
65
+ // Draw drops
66
+ for (let i = 0; i < drops.length; i++) {
67
+ // Random character
68
+ const text = matrixArray[Math.floor(Math.random() * matrixArray.length)]
69
+
70
+ // Draw the character
71
+ ctx.fillText(text, i * fontSize, drops[i] * fontSize)
72
+
73
+ // Reset drop when it goes off screen
74
+ if (drops[i] * fontSize > canvas.height && Math.random() > 0.975) {
75
+ drops[i] = 0
76
+ }
77
+
78
+ // Move drop down
79
+ drops[i]++
80
+ }
81
+ }
82
+
83
+ // Animation loop
84
+ const interval = setInterval(draw, 35)
85
+
86
+ // Cleanup
87
+ return () => {
88
+ clearInterval(interval)
89
+ clearTimeout(specialWordTimer)
90
+ setShowText(false)
91
+ }
92
+ }, [isActive])
93
+
94
+ // Handle window resize
95
+ useEffect(() => {
96
+ if (!isActive) return
97
+
98
+ const handleResize = () => {
99
+ const canvas = canvasRef.current
100
+ if (canvas) {
101
+ canvas.width = window.innerWidth
102
+ canvas.height = window.innerHeight
103
+ }
104
+ }
105
+
106
+ window.addEventListener('resize', handleResize)
107
+ return () => window.removeEventListener('resize', handleResize)
108
+ }, [isActive])
109
+
110
+ // Auto-complete after 5 seconds
111
+ useEffect(() => {
112
+ if (!isActive) return
113
+
114
+ const timer = setTimeout(() => {
115
+ if (onComplete) {
116
+ onComplete()
117
+ }
118
+ }, 5000)
119
+
120
+ return () => clearTimeout(timer)
121
+ }, [isActive, onComplete])
122
+
123
+ return (
124
+ <AnimatePresence>
125
+ {isActive && (
126
+ <>
127
+ <motion.div
128
+ initial={{ opacity: 0 }}
129
+ animate={{ opacity: 1 }}
130
+ exit={{ opacity: 0 }}
131
+ transition={{ duration: 0.5 }}
132
+ className="fixed inset-0 z-[9999] bg-black"
133
+ >
134
+ <canvas
135
+ ref={canvasRef}
136
+ className="absolute inset-0"
137
+ style={{ display: 'block' }}
138
+ />
139
+
140
+ {/* REUBENOS Text */}
141
+ <AnimatePresence>
142
+ {showText && (
143
+ <motion.div
144
+ initial={{ opacity: 0, scale: 0.5 }}
145
+ animate={{ opacity: 1, scale: 1 }}
146
+ exit={{ opacity: 0, scale: 0.8 }}
147
+ transition={{
148
+ duration: 0.8,
149
+ ease: 'easeOut'
150
+ }}
151
+ className="absolute inset-0 flex items-center justify-center pointer-events-none"
152
+ >
153
+ <div className="text-center">
154
+ <motion.h1
155
+ className="text-[#0F0] font-mono font-bold select-none"
156
+ style={{
157
+ fontSize: 'clamp(3rem, 10vw, 8rem)',
158
+ textShadow: `
159
+ 0 0 10px #0F0,
160
+ 0 0 20px #0F0,
161
+ 0 0 30px #0F0,
162
+ 0 0 40px #0A0,
163
+ 0 0 70px #0A0,
164
+ 0 0 80px #0A0,
165
+ 0 0 100px #0A0,
166
+ 0 0 150px #0A0
167
+ `,
168
+ letterSpacing: '0.2em'
169
+ }}
170
+ initial={{ y: 20 }}
171
+ animate={{
172
+ y: [20, -10, 0],
173
+ textShadow: [
174
+ `0 0 10px #0F0, 0 0 20px #0F0, 0 0 30px #0F0, 0 0 40px #0A0, 0 0 70px #0A0, 0 0 80px #0A0, 0 0 100px #0A0, 0 0 150px #0A0`,
175
+ `0 0 20px #0F0, 0 0 30px #0F0, 0 0 40px #0F0, 0 0 50px #0A0, 0 0 80px #0A0, 0 0 90px #0A0, 0 0 120px #0A0, 0 0 180px #0A0`,
176
+ `0 0 10px #0F0, 0 0 20px #0F0, 0 0 30px #0F0, 0 0 40px #0A0, 0 0 70px #0A0, 0 0 80px #0A0, 0 0 100px #0A0, 0 0 150px #0A0`
177
+ ]
178
+ }}
179
+ transition={{
180
+ duration: 2,
181
+ repeat: Infinity,
182
+ repeatType: 'reverse',
183
+ ease: 'easeInOut'
184
+ }}
185
+ >
186
+ REUBENOS
187
+ </motion.h1>
188
+
189
+ <motion.div
190
+ initial={{ opacity: 0 }}
191
+ animate={{ opacity: [0, 1, 0] }}
192
+ transition={{
193
+ duration: 1.5,
194
+ repeat: Infinity,
195
+ repeatDelay: 0.5
196
+ }}
197
+ className="mt-8 text-[#0F0] font-mono text-xl"
198
+ style={{
199
+ textShadow: '0 0 5px #0F0, 0 0 10px #0F0'
200
+ }}
201
+ >
202
+ SYSTEM BREACH DETECTED
203
+ </motion.div>
204
+
205
+ <motion.div
206
+ initial={{ width: 0 }}
207
+ animate={{ width: '100%' }}
208
+ transition={{
209
+ duration: 3,
210
+ ease: 'linear'
211
+ }}
212
+ className="mt-4 h-1 bg-gradient-to-r from-transparent via-[#0F0] to-transparent mx-auto max-w-md"
213
+ style={{
214
+ boxShadow: '0 0 10px #0F0'
215
+ }}
216
+ />
217
+ </div>
218
+ </motion.div>
219
+ )}
220
+ </AnimatePresence>
221
+
222
+ {/* Click to exit hint */}
223
+ <motion.div
224
+ initial={{ opacity: 0 }}
225
+ animate={{ opacity: 1 }}
226
+ transition={{ delay: 3, duration: 1 }}
227
+ className="absolute bottom-10 left-1/2 transform -translate-x-1/2 text-[#0F0] font-mono text-sm opacity-50"
228
+ >
229
+ Click anywhere to exit the Matrix
230
+ </motion.div>
231
+ </motion.div>
232
+ </>
233
+ )}
234
+ </AnimatePresence>
235
+ )
236
+ }
app/components/PDFViewer.tsx ADDED
@@ -0,0 +1,143 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ 'use client'
2
+
3
+ import React, { useState } from 'react'
4
+ import dynamic from 'next/dynamic'
5
+ import { MagnifyingGlassPlus, MagnifyingGlassMinus, ArrowLeft, ArrowRight } from '@phosphor-icons/react'
6
+
7
+ // Dynamically import react-pdf with no SSR
8
+ const Document = dynamic(
9
+ () => import('react-pdf').then((mod) => mod.Document),
10
+ { ssr: false }
11
+ )
12
+ const Page = dynamic(
13
+ () => import('react-pdf').then((mod) => mod.Page),
14
+ { ssr: false }
15
+ )
16
+
17
+ // Configure PDF.js worker
18
+ if (typeof window !== 'undefined') {
19
+ import('react-pdf').then((pdfjs) => {
20
+ pdfjs.pdfjs.GlobalWorkerOptions.workerSrc = `//unpkg.com/pdfjs-dist@${pdfjs.pdfjs.version}/build/pdf.worker.min.mjs`
21
+ })
22
+ }
23
+
24
+ interface PDFViewerProps {
25
+ url: string
26
+ scale: number
27
+ onZoomIn: () => void
28
+ onZoomOut: () => void
29
+ }
30
+
31
+ export function PDFViewer({ url, scale, onZoomIn, onZoomOut }: PDFViewerProps) {
32
+ const [numPages, setNumPages] = useState<number | null>(null)
33
+ const [pageNumber, setPageNumber] = useState(1)
34
+ const [error, setError] = useState<string | null>(null)
35
+ const [isLoading, setIsLoading] = useState(true)
36
+
37
+ const onDocumentLoadSuccess = ({ numPages }: { numPages: number }) => {
38
+ setNumPages(numPages)
39
+ setPageNumber(1)
40
+ setError(null)
41
+ setIsLoading(false)
42
+ }
43
+
44
+ const changePage = (offset: number) => {
45
+ setPageNumber(prevPageNumber => {
46
+ const newPageNumber = prevPageNumber + offset
47
+ if (newPageNumber < 1 || newPageNumber > (numPages || 1)) {
48
+ return prevPageNumber
49
+ }
50
+ return newPageNumber
51
+ })
52
+ }
53
+
54
+ if (!url) {
55
+ return <div className="flex items-center justify-center h-full text-gray-500">No PDF to display</div>
56
+ }
57
+
58
+ return (
59
+ <div className="flex flex-col h-full">
60
+ <div className="flex items-center gap-2 p-3 border-b border-gray-200 bg-gray-50">
61
+ <button
62
+ onClick={onZoomOut}
63
+ className="p-2 hover:bg-gray-200 rounded-lg transition-colors"
64
+ title="Zoom Out"
65
+ >
66
+ <MagnifyingGlassMinus className="h-5 w-5" />
67
+ </button>
68
+ <span className="text-sm text-gray-600 min-w-[60px] text-center">{Math.round(scale * 100)}%</span>
69
+ <button
70
+ onClick={onZoomIn}
71
+ className="p-2 hover:bg-gray-200 rounded-lg transition-colors"
72
+ title="Zoom In"
73
+ >
74
+ <MagnifyingGlassPlus className="h-5 w-5" />
75
+ </button>
76
+ {numPages && (
77
+ <>
78
+ <div className="mx-4 h-6 w-px bg-gray-300" />
79
+ <button
80
+ onClick={() => changePage(-1)}
81
+ disabled={pageNumber <= 1}
82
+ className="p-2 hover:bg-gray-200 rounded-lg transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
83
+ title="Previous Page"
84
+ >
85
+ <ArrowLeft className="h-5 w-5" />
86
+ </button>
87
+ <span className="text-sm text-gray-600 min-w-[80px] text-center">
88
+ Page {pageNumber} of {numPages}
89
+ </span>
90
+ <button
91
+ onClick={() => changePage(1)}
92
+ disabled={pageNumber >= numPages}
93
+ className="p-2 hover:bg-gray-200 rounded-lg transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
94
+ title="Next Page"
95
+ >
96
+ <ArrowRight className="h-5 w-5" />
97
+ </button>
98
+ </>
99
+ )}
100
+ </div>
101
+ <div className="flex-1 overflow-auto p-4 bg-gray-50">
102
+ {typeof window !== 'undefined' && url && (
103
+ <Document
104
+ file={url}
105
+ onLoadSuccess={onDocumentLoadSuccess}
106
+ onLoadError={(err) => {
107
+ console.warn('PDF load error:', err)
108
+ setError('Unable to load PDF. The file might be corrupted or in an unsupported format.')
109
+ setIsLoading(false)
110
+ }}
111
+ loading={
112
+ <div className="flex items-center justify-center h-full">
113
+ <div className="text-gray-600">Loading PDF...</div>
114
+ </div>
115
+ }
116
+ error={
117
+ <div className="flex flex-col items-center justify-center h-full">
118
+ <div className="text-red-500 mb-2">Failed to load PDF</div>
119
+ <div className="text-sm text-gray-600">{error || 'The file might be corrupted or in an unsupported format.'}</div>
120
+ </div>
121
+ }
122
+ className="flex justify-center"
123
+ >
124
+ {!isLoading && !error && (
125
+ <Page
126
+ pageNumber={pageNumber}
127
+ scale={scale}
128
+ renderTextLayer={false}
129
+ renderAnnotationLayer={false}
130
+ className="shadow-lg bg-white"
131
+ loading={
132
+ <div className="flex items-center justify-center h-96 bg-white shadow-lg">
133
+ <div className="text-gray-400">Loading page...</div>
134
+ </div>
135
+ }
136
+ />
137
+ )}
138
+ </Document>
139
+ )}
140
+ </div>
141
+ </div>
142
+ )
143
+ }
app/components/SpotlightSearch.tsx ADDED
@@ -0,0 +1,182 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ 'use client'
2
+
3
+ import React, { useState, useRef, useEffect } from 'react'
4
+ import { MagnifyingGlass, X } from '@phosphor-icons/react'
5
+ import { motion, AnimatePresence } from 'framer-motion'
6
+
7
+ interface SpotlightSearchProps {
8
+ isOpen: boolean
9
+ onClose: () => void
10
+ onOpenApp: (appId: string) => void
11
+ }
12
+
13
+ interface SearchResult {
14
+ id: string
15
+ name: string
16
+ type: 'app' | 'file' | 'setting'
17
+ icon?: React.ReactNode
18
+ }
19
+
20
+ export function SpotlightSearch({ isOpen, onClose, onOpenApp }: SpotlightSearchProps) {
21
+ const [query, setQuery] = useState('')
22
+ const [results, setResults] = useState<SearchResult[]>([])
23
+ const inputRef = useRef<HTMLInputElement>(null)
24
+
25
+ const apps: SearchResult[] = [
26
+ { id: 'files', name: 'Files', type: 'app' },
27
+ { id: 'calendar', name: 'Calendar', type: 'app' },
28
+ { id: 'clock', name: 'Clock', type: 'app' },
29
+ { id: 'browser', name: 'Web Browser', type: 'app' },
30
+ { id: 'gemini', name: 'Gemini Chat', type: 'app' },
31
+ { id: 'terminal', name: 'Terminal', type: 'app' },
32
+ { id: 'vscode', name: 'VS Code', type: 'app' },
33
+ ]
34
+
35
+ const files: SearchResult[] = [
36
+ { id: 'document', name: 'Document.pdf', type: 'file' },
37
+ { id: 'resume', name: 'Resume.docx', type: 'file' },
38
+ { id: 'budget', name: 'Budget.xlsx', type: 'file' },
39
+ { id: 'presentation', name: 'Presentation.pptx', type: 'file' },
40
+ ]
41
+
42
+ const settings: SearchResult[] = [
43
+ { id: 'wallpaper', name: 'Change Wallpaper', type: 'setting' },
44
+ { id: 'theme', name: 'Dark Mode', type: 'setting' },
45
+ { id: 'display', name: 'Display Settings', type: 'setting' },
46
+ ]
47
+
48
+ const allItems = [...apps, ...files, ...settings]
49
+
50
+ useEffect(() => {
51
+ if (isOpen) {
52
+ setQuery('')
53
+ setResults([])
54
+ setTimeout(() => inputRef.current?.focus(), 100)
55
+ }
56
+ }, [isOpen])
57
+
58
+ useEffect(() => {
59
+ if (query.trim() === '') {
60
+ setResults([])
61
+ return
62
+ }
63
+
64
+ const filtered = allItems.filter(item =>
65
+ item.name.toLowerCase().includes(query.toLowerCase())
66
+ )
67
+ setResults(filtered.slice(0, 8))
68
+ }, [query])
69
+
70
+ const handleSelect = (result: SearchResult) => {
71
+ if (result.type === 'app') {
72
+ onOpenApp(result.id)
73
+ }
74
+ onClose()
75
+ setQuery('')
76
+ }
77
+
78
+ const handleKeyDown = (e: React.KeyboardEvent) => {
79
+ if (e.key === 'Escape') {
80
+ onClose()
81
+ } else if (e.key === 'Enter' && results.length > 0) {
82
+ handleSelect(results[0])
83
+ }
84
+ }
85
+
86
+ return (
87
+ <AnimatePresence>
88
+ {isOpen && (
89
+ <>
90
+ {/* Backdrop */}
91
+ <motion.div
92
+ initial={{ opacity: 0 }}
93
+ animate={{ opacity: 1 }}
94
+ exit={{ opacity: 0 }}
95
+ className="fixed inset-0 bg-black/20 z-[60]"
96
+ onClick={onClose}
97
+ />
98
+
99
+ {/* Search Box */}
100
+ <motion.div
101
+ initial={{ opacity: 0, scale: 0.95, y: -20 }}
102
+ animate={{ opacity: 1, scale: 1, y: 0 }}
103
+ exit={{ opacity: 0, scale: 0.95, y: -20 }}
104
+ transition={{ duration: 0.15, ease: 'easeOut' }}
105
+ className="fixed top-[20%] left-1/2 -translate-x-1/2 w-[600px] max-w-[90%] glass rounded-xl shadow-2xl z-[70]"
106
+ >
107
+ <div className="flex items-center px-4 py-3 gap-3 border-b border-gray-200/20">
108
+ <MagnifyingGlass size={24} weight="regular" className="text-gray-600" />
109
+ <input
110
+ ref={inputRef}
111
+ type="text"
112
+ value={query}
113
+ onChange={(e) => setQuery(e.target.value)}
114
+ onKeyDown={handleKeyDown}
115
+ className="flex-1 bg-transparent text-xl focus:outline-none text-gray-800 placeholder-gray-400"
116
+ placeholder="Spotlight Search"
117
+ autoComplete="off"
118
+ spellCheck={false}
119
+ />
120
+ <button
121
+ onClick={onClose}
122
+ className="text-gray-500 hover:text-gray-700 transition-colors"
123
+ >
124
+ <X size={20} weight="regular" />
125
+ </button>
126
+ </div>
127
+
128
+ {/* Results */}
129
+ {results.length > 0 && (
130
+ <div className="max-h-96 overflow-y-auto p-2">
131
+ {results.map((result, index) => (
132
+ <div
133
+ key={result.id}
134
+ onClick={() => handleSelect(result)}
135
+ className={`flex items-center px-3 py-2.5 hover:bg-blue-500 hover:text-white rounded-lg cursor-pointer transition-colors ${
136
+ index === 0 ? 'bg-blue-500/10' : ''
137
+ }`}
138
+ >
139
+ <div className="flex-1 flex flex-col">
140
+ <span className="font-medium">{result.name}</span>
141
+ <span className="text-xs opacity-70 capitalize">{result.type}</span>
142
+ </div>
143
+ {result.type === 'app' && (
144
+ <span className="text-xs opacity-50">⌘ Enter</span>
145
+ )}
146
+ </div>
147
+ ))}
148
+ </div>
149
+ )}
150
+
151
+ {/* No results */}
152
+ {query.trim() !== '' && results.length === 0 && (
153
+ <div className="p-4 text-center text-gray-500">
154
+ No results found for "{query}"
155
+ </div>
156
+ )}
157
+
158
+ {/* Quick Actions (when no query) */}
159
+ {query === '' && (
160
+ <div className="p-4">
161
+ <div className="text-xs font-semibold text-gray-500 uppercase tracking-wider mb-2">
162
+ Suggestions
163
+ </div>
164
+ <div className="grid grid-cols-2 gap-2">
165
+ {apps.slice(0, 4).map(app => (
166
+ <div
167
+ key={app.id}
168
+ onClick={() => handleSelect(app)}
169
+ className="flex items-center px-3 py-2 hover:bg-gray-100 rounded-lg cursor-pointer transition-colors"
170
+ >
171
+ <span className="text-sm">{app.name}</span>
172
+ </div>
173
+ ))}
174
+ </div>
175
+ </div>
176
+ )}
177
+ </motion.div>
178
+ </>
179
+ )}
180
+ </AnimatePresence>
181
+ )
182
+ }
app/components/Terminal.tsx ADDED
@@ -0,0 +1,223 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ 'use client'
2
+
3
+ import React, { useState, useRef, useEffect } from 'react'
4
+ import Window from './Window'
5
+
6
+ interface TerminalProps {
7
+ onClose: () => void
8
+ }
9
+
10
+ interface FileSystem {
11
+ [key: string]: string[]
12
+ }
13
+
14
+ export function Terminal({ onClose }: TerminalProps) {
15
+ const [history, setHistory] = useState<string[]>([
16
+ 'Last login: ' + new Date().toLocaleString() + ' on ttys000'
17
+ ])
18
+ const [currentInput, setCurrentInput] = useState('')
19
+ const [currentPath, setCurrentPath] = useState('~')
20
+ const inputRef = useRef<HTMLInputElement>(null)
21
+ const outputRef = useRef<HTMLDivElement>(null)
22
+
23
+ const fileSystem: FileSystem = {
24
+ '~': ['Desktop', 'Documents', 'Downloads', 'Pictures', 'Music', 'Videos'],
25
+ 'Desktop': ['screenshot.png', 'notes.txt', 'project'],
26
+ 'Documents': ['resume.pdf', 'budget.xlsx', 'report.docx'],
27
+ 'Downloads': ['installer.dmg', 'image.jpg', 'archive.zip'],
28
+ 'Pictures': ['vacation.jpg', 'family.png'],
29
+ 'Music': ['song.mp3', 'playlist.m3u'],
30
+ 'Videos': ['tutorial.mp4', 'presentation.mov'],
31
+ 'project': ['index.html', 'style.css', 'script.js', 'README.md']
32
+ }
33
+
34
+ useEffect(() => {
35
+ if (outputRef.current) {
36
+ outputRef.current.scrollTop = outputRef.current.scrollHeight
37
+ }
38
+ }, [history])
39
+
40
+ const handleCommand = (e: React.KeyboardEvent<HTMLInputElement>) => {
41
+ if (e.key === 'Enter') {
42
+ const cmd = currentInput.trim()
43
+ const newHistory = [...history]
44
+
45
+ // Add command to history
46
+ newHistory.push(`guest@studyos:${currentPath} $ ${cmd}`)
47
+
48
+ // Process command
49
+ const args = cmd.split(' ')
50
+ const command = args[0].toLowerCase()
51
+
52
+ switch (command) {
53
+ case 'help':
54
+ newHistory.push('Available commands:')
55
+ newHistory.push(' help - Show this help message')
56
+ newHistory.push(' ls - List directory contents')
57
+ newHistory.push(' cd - Change directory')
58
+ newHistory.push(' pwd - Print working directory')
59
+ newHistory.push(' clear - Clear terminal')
60
+ newHistory.push(' date - Show current date and time')
61
+ newHistory.push(' whoami - Display current user')
62
+ newHistory.push(' echo - Display message')
63
+ newHistory.push(' cat - Display file contents')
64
+ newHistory.push(' mkdir - Create directory')
65
+ newHistory.push(' touch - Create file')
66
+ newHistory.push(' rm - Remove file')
67
+ newHistory.push(' open - Open application')
68
+ break
69
+
70
+ case 'ls':
71
+ const files = fileSystem[currentPath] || []
72
+ if (files.length > 0) {
73
+ const fileList = files.map(f => {
74
+ const isDir = fileSystem[f] !== undefined
75
+ return isDir ?
76
+ `\x1b[34m${f}/\x1b[0m` : // Blue for directories
77
+ f
78
+ }).join(' ')
79
+ newHistory.push(fileList)
80
+ } else {
81
+ newHistory.push('Directory is empty')
82
+ }
83
+ break
84
+
85
+ case 'cd':
86
+ if (args[1] === '..') {
87
+ setCurrentPath('~')
88
+ } else if (args[1] === '~' || !args[1]) {
89
+ setCurrentPath('~')
90
+ } else if (fileSystem[args[1]]) {
91
+ setCurrentPath(args[1])
92
+ } else {
93
+ newHistory.push(`cd: no such file or directory: ${args[1]}`)
94
+ }
95
+ break
96
+
97
+ case 'pwd':
98
+ newHistory.push(currentPath === '~' ? '/home/guest' : `/home/guest/${currentPath}`)
99
+ break
100
+
101
+ case 'clear':
102
+ setHistory(['Last login: ' + new Date().toLocaleString() + ' on ttys000'])
103
+ setCurrentInput('')
104
+ return
105
+
106
+ case 'date':
107
+ newHistory.push(new Date().toString())
108
+ break
109
+
110
+ case 'whoami':
111
+ newHistory.push('guest')
112
+ break
113
+
114
+ case 'echo':
115
+ newHistory.push(args.slice(1).join(' '))
116
+ break
117
+
118
+ case 'cat':
119
+ if (args[1]) {
120
+ const files = fileSystem[currentPath] || []
121
+ if (files.includes(args[1])) {
122
+ newHistory.push(`Contents of ${args[1]}:`)
123
+ newHistory.push('(This is a simulated file content)')
124
+ } else {
125
+ newHistory.push(`cat: ${args[1]}: No such file`)
126
+ }
127
+ } else {
128
+ newHistory.push('Usage: cat <filename>')
129
+ }
130
+ break
131
+
132
+ case 'mkdir':
133
+ if (args[1]) {
134
+ newHistory.push(`Directory '${args[1]}' created`)
135
+ } else {
136
+ newHistory.push('Usage: mkdir <dirname>')
137
+ }
138
+ break
139
+
140
+ case 'touch':
141
+ if (args[1]) {
142
+ newHistory.push(`File '${args[1]}' created`)
143
+ } else {
144
+ newHistory.push('Usage: touch <filename>')
145
+ }
146
+ break
147
+
148
+ case 'rm':
149
+ if (args[1]) {
150
+ newHistory.push(`File '${args[1]}' removed`)
151
+ } else {
152
+ newHistory.push('Usage: rm <filename>')
153
+ }
154
+ break
155
+
156
+ case 'open':
157
+ if (args[1]) {
158
+ newHistory.push(`Opening ${args[1]}...`)
159
+ } else {
160
+ newHistory.push('Usage: open <application>')
161
+ }
162
+ break
163
+
164
+ case '':
165
+ break
166
+
167
+ default:
168
+ newHistory.push(`zsh: command not found: ${command}`)
169
+ }
170
+
171
+ setHistory(newHistory)
172
+ setCurrentInput('')
173
+ }
174
+ }
175
+
176
+ return (
177
+ <Window
178
+ id="terminal"
179
+ title="Terminal — zsh — 80×24"
180
+ isOpen={true}
181
+ onClose={onClose}
182
+ width={600}
183
+ height={400}
184
+ x={120}
185
+ y={120}
186
+ darkMode={true}
187
+ className="terminal-window"
188
+ >
189
+ <div
190
+ className="flex-1 bg-[#1e1e1e] p-2 font-mono text-sm text-white overflow-auto"
191
+ ref={outputRef}
192
+ onClick={() => inputRef.current?.focus()}
193
+ >
194
+ {history.map((line, index) => (
195
+ <div
196
+ key={index}
197
+ className="whitespace-pre-wrap"
198
+ dangerouslySetInnerHTML={{
199
+ __html: line
200
+ .replace(/\x1b\[34m/g, '<span style="color: #4FC3F7;">')
201
+ .replace(/\x1b\[0m/g, '</span>')
202
+ .replace(/guest@studyos/g, '<span style="color: #4CAF50;">guest@studyos</span>')
203
+ }}
204
+ />
205
+ ))}
206
+ <div className="flex">
207
+ <span className="text-green-400 mr-2">guest@studyos:{currentPath} $</span>
208
+ <input
209
+ ref={inputRef}
210
+ type="text"
211
+ value={currentInput}
212
+ onChange={(e) => setCurrentInput(e.target.value)}
213
+ onKeyDown={handleCommand}
214
+ className="flex-1 bg-transparent border-none outline-none text-white"
215
+ autoFocus
216
+ autoComplete="off"
217
+ spellCheck={false}
218
+ />
219
+ </div>
220
+ </div>
221
+ </Window>
222
+ )
223
+ }
app/components/TopBar.tsx ADDED
@@ -0,0 +1,122 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ 'use client'
2
+
3
+ import React, { useState, useEffect } from 'react'
4
+ import {
5
+ SpeakerHigh,
6
+ WifiHigh,
7
+ BatteryFull,
8
+ MagnifyingGlass,
9
+ Desktop,
10
+ CirclesFour
11
+ } from '@phosphor-icons/react'
12
+ import { motion, AnimatePresence } from 'framer-motion'
13
+
14
+ interface TopBarProps {
15
+ onPowerAction?: () => void
16
+ activeAppName?: string
17
+ onAboutClick?: () => void
18
+ }
19
+
20
+ export function TopBar({ onPowerAction, activeAppName = 'Finder', onAboutClick }: TopBarProps) {
21
+ const [appleMenuOpen, setAppleMenuOpen] = useState(false)
22
+ const [currentTime, setCurrentTime] = useState('')
23
+
24
+ useEffect(() => {
25
+ const updateTime = () => {
26
+ const now = new Date()
27
+ const days = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']
28
+ const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']
29
+
30
+ const timeString = now.toLocaleTimeString('en-US', {
31
+ hour: 'numeric',
32
+ minute: '2-digit',
33
+ hour12: true
34
+ })
35
+ const dateString = `${days[now.getDay()]} ${now.getDate()} ${months[now.getMonth()]}`
36
+ setCurrentTime(`${dateString} ${timeString}`)
37
+ }
38
+
39
+ updateTime()
40
+ const interval = setInterval(updateTime, 1000)
41
+ return () => clearInterval(interval)
42
+ }, [])
43
+
44
+ return (
45
+ <div className="fixed top-0 left-0 right-0 h-8 glass flex items-center justify-between px-4 text-sm z-50 shadow-sm">
46
+ <div className="flex items-center space-x-4">
47
+ <button
48
+ onClick={() => setAppleMenuOpen(!appleMenuOpen)}
49
+ className="font-bold text-lg hover:text-blue-600 transition-colors relative"
50
+ >
51
+ <CirclesFour size={20} weight="fill" className="text-blue-600" />
52
+ </button>
53
+ <span className="font-bold text-gray-800">Reuben OS</span>
54
+ <span className="text-gray-600 text-xs ml-2">{activeAppName}</span>
55
+ </div>
56
+
57
+ <div className="flex items-center space-x-4">
58
+ <BatteryFull size={16} weight="fill" className="text-gray-700" />
59
+ <WifiHigh size={16} weight="fill" className="text-gray-700" />
60
+ <MagnifyingGlass size={16} weight="regular" className="text-gray-700" />
61
+ <div className="font-medium text-gray-800">{currentTime}</div>
62
+ </div>
63
+
64
+ <AnimatePresence>
65
+ {appleMenuOpen && (
66
+ <>
67
+ <div
68
+ className="fixed inset-0 z-[55]"
69
+ onClick={() => setAppleMenuOpen(false)}
70
+ />
71
+ <motion.div
72
+ initial={{ opacity: 0, y: -5 }}
73
+ animate={{ opacity: 1, y: 0 }}
74
+ exit={{ opacity: 0, y: -5 }}
75
+ transition={{ duration: 0.15 }}
76
+ className="absolute top-8 left-2 w-48 glass rounded-lg shadow-xl flex flex-col py-1 z-[60] text-sm"
77
+ >
78
+ <button
79
+ onClick={() => {
80
+ if (onAboutClick) onAboutClick()
81
+ setAppleMenuOpen(false)
82
+ }}
83
+ className="text-left px-4 py-1 hover:bg-blue-500 hover:text-white transition-colors text-gray-800">
84
+ About Reuben OS
85
+ </button>
86
+ <div className="h-px bg-gray-300 my-1 mx-2" />
87
+ <button className="text-left px-4 py-1 hover:bg-blue-500 hover:text-white transition-colors text-gray-800">
88
+ System Settings...
89
+ </button>
90
+ <div className="h-px bg-gray-300 my-1 mx-2" />
91
+ <button className="text-left px-4 py-1 hover:bg-blue-500 hover:text-white transition-colors text-gray-800">
92
+ Sleep
93
+ </button>
94
+ <button className="text-left px-4 py-1 hover:bg-blue-500 hover:text-white transition-colors text-gray-800">
95
+ Restart...
96
+ </button>
97
+ <button
98
+ onClick={() => {
99
+ if (onPowerAction) onPowerAction()
100
+ setAppleMenuOpen(false)
101
+ }}
102
+ className="text-left px-4 py-1 hover:bg-blue-500 hover:text-white transition-colors text-gray-800"
103
+ >
104
+ Shut Down...
105
+ </button>
106
+ <div className="h-px bg-gray-300 my-1 mx-2" />
107
+ <button
108
+ onClick={() => {
109
+ if (onPowerAction) onPowerAction()
110
+ setAppleMenuOpen(false)
111
+ }}
112
+ className="text-left px-4 py-1 hover:bg-blue-500 hover:text-white transition-colors text-gray-800"
113
+ >
114
+ Log Out...
115
+ </button>
116
+ </motion.div>
117
+ </>
118
+ )}
119
+ </AnimatePresence>
120
+ </div>
121
+ )
122
+ }
app/components/VSCodeEditor.tsx ADDED
@@ -0,0 +1,458 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ 'use client'
2
+
3
+ import React, { useState, useRef, useEffect } from 'react'
4
+ import Window from './Window'
5
+ import Editor from '@monaco-editor/react'
6
+ import {
7
+ Code,
8
+ Play,
9
+ FileHtml,
10
+ FileCss,
11
+ FileJs,
12
+ Download,
13
+ Upload,
14
+ FloppyDisk,
15
+ Eye,
16
+ EyeSlash,
17
+ ArrowsOutSimple,
18
+ ArrowsInSimple,
19
+ X
20
+ } from '@phosphor-icons/react'
21
+
22
+ interface VSCodeEditorProps {
23
+ onClose: () => void
24
+ userSession?: string
25
+ }
26
+
27
+ interface Tab {
28
+ id: string
29
+ name: string
30
+ language: string
31
+ content: string
32
+ }
33
+
34
+ export function VSCodeEditor({ onClose, userSession }: VSCodeEditorProps) {
35
+ const [activeTab, setActiveTab] = useState('html')
36
+ const [showPreview, setShowPreview] = useState(true)
37
+ const [isFullscreen, setIsFullscreen] = useState(false)
38
+ const [isSaving, setIsSaving] = useState(false)
39
+ const previewRef = useRef<HTMLIFrameElement>(null)
40
+
41
+ // Generate unique session ID if not provided
42
+ const sessionId = userSession || `session_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`
43
+
44
+ // Function to get icon for each tab
45
+ const getTabIcon = (tabId: string) => {
46
+ switch (tabId) {
47
+ case 'html':
48
+ return <FileHtml size={16} weight="fill" className="text-orange-500" />
49
+ case 'css':
50
+ return <FileCss size={16} weight="fill" className="text-blue-500" />
51
+ case 'js':
52
+ return <FileJs size={16} weight="fill" className="text-yellow-500" />
53
+ default:
54
+ return <Code size={16} weight="fill" className="text-gray-500" />
55
+ }
56
+ }
57
+
58
+ const [tabs, setTabs] = useState<Tab[]>([
59
+ {
60
+ id: 'html',
61
+ name: 'index.html',
62
+ language: 'html',
63
+ content: `<!DOCTYPE html>
64
+ <html lang="en">
65
+ <head>
66
+ <meta charset="UTF-8">
67
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
68
+ <title>My Awesome Page</title>
69
+ <link rel="stylesheet" href="style.css">
70
+ </head>
71
+ <body>
72
+ <div class="container">
73
+ <h1>Welcome to Reuben OS Editor!</h1>
74
+ <p>Start coding and see your changes live!</p>
75
+ <button onclick="showMessage()">Click Me!</button>
76
+ <div id="output"></div>
77
+ </div>
78
+ <script src="script.js"></script>
79
+ </body>
80
+ </html>`
81
+ },
82
+ {
83
+ id: 'css',
84
+ name: 'style.css',
85
+ language: 'css',
86
+ content: `* {
87
+ margin: 0;
88
+ padding: 0;
89
+ box-sizing: border-box;
90
+ }
91
+
92
+ body {
93
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
94
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
95
+ min-height: 100vh;
96
+ display: flex;
97
+ align-items: center;
98
+ justify-content: center;
99
+ }
100
+
101
+ .container {
102
+ background: rgba(255, 255, 255, 0.95);
103
+ padding: 40px;
104
+ border-radius: 20px;
105
+ box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
106
+ text-align: center;
107
+ max-width: 500px;
108
+ }
109
+
110
+ h1 {
111
+ color: #333;
112
+ margin-bottom: 20px;
113
+ font-size: 2.5em;
114
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
115
+ -webkit-background-clip: text;
116
+ -webkit-text-fill-color: transparent;
117
+ }
118
+
119
+ p {
120
+ color: #666;
121
+ margin-bottom: 30px;
122
+ font-size: 1.1em;
123
+ }
124
+
125
+ button {
126
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
127
+ color: white;
128
+ border: none;
129
+ padding: 12px 30px;
130
+ border-radius: 50px;
131
+ font-size: 16px;
132
+ cursor: pointer;
133
+ transition: all 0.3s ease;
134
+ box-shadow: 0 4px 15px rgba(102, 126, 234, 0.4);
135
+ }
136
+
137
+ button:hover {
138
+ transform: translateY(-2px);
139
+ box-shadow: 0 6px 20px rgba(102, 126, 234, 0.6);
140
+ }
141
+
142
+ #output {
143
+ margin-top: 20px;
144
+ padding: 15px;
145
+ background: #f7f7f7;
146
+ border-radius: 10px;
147
+ min-height: 50px;
148
+ font-family: 'Courier New', monospace;
149
+ }`
150
+ },
151
+ {
152
+ id: 'js',
153
+ name: 'script.js',
154
+ language: 'javascript',
155
+ content: `// Welcome to Reuben OS Code Editor!
156
+
157
+ function showMessage() {
158
+ const messages = [
159
+ "Hello from Reuben OS! 🚀",
160
+ "You're doing great! 💪",
161
+ "Keep coding! 🎉",
162
+ "Awesome work! ⭐",
163
+ "Reuben OS is amazing! 🌟"
164
+ ];
165
+
166
+ const randomMessage = messages[Math.floor(Math.random() * messages.length)];
167
+ const output = document.getElementById('output');
168
+
169
+ output.innerHTML = \`
170
+ <strong>Message:</strong> \${randomMessage}<br>
171
+ <small>Generated at: \${new Date().toLocaleTimeString()}</small>
172
+ \`;
173
+
174
+ // Add animation
175
+ output.style.opacity = '0';
176
+ setTimeout(() => {
177
+ output.style.transition = 'opacity 0.5s ease';
178
+ output.style.opacity = '1';
179
+ }, 10);
180
+ }
181
+
182
+ // Log to console when page loads
183
+ console.log('Reuben OS Editor initialized!');
184
+ console.log('Session ID:', '\${sessionId}');`
185
+ }
186
+ ])
187
+
188
+ // Load saved code from localStorage based on session
189
+ useEffect(() => {
190
+ const savedCode = localStorage.getItem(`vscode_${sessionId}`)
191
+ if (savedCode) {
192
+ try {
193
+ const parsed = JSON.parse(savedCode)
194
+ setTabs(parsed)
195
+ } catch (e) {
196
+ console.error('Failed to load saved code')
197
+ }
198
+ }
199
+ }, [sessionId])
200
+
201
+ // Save code to localStorage
202
+ useEffect(() => {
203
+ if (tabs.length > 0) {
204
+ localStorage.setItem(`vscode_${sessionId}`, JSON.stringify(tabs))
205
+ }
206
+ }, [tabs, sessionId])
207
+
208
+ // Update preview when code changes
209
+ useEffect(() => {
210
+ if (previewRef.current && showPreview) {
211
+ updatePreview()
212
+ }
213
+ }, [tabs, showPreview])
214
+
215
+ const updatePreview = () => {
216
+ if (!previewRef.current) return
217
+
218
+ const htmlTab = tabs.find(t => t.id === 'html')
219
+ const cssTab = tabs.find(t => t.id === 'css')
220
+ const jsTab = tabs.find(t => t.id === 'js')
221
+
222
+ const previewContent = `
223
+ <!DOCTYPE html>
224
+ <html>
225
+ <head>
226
+ <style>${cssTab?.content || ''}</style>
227
+ </head>
228
+ <body>
229
+ ${htmlTab?.content.replace(/<link.*?>/g, '').replace(/<script.*?src=.*?><\/script>/g, '') || ''}
230
+ <script>
231
+ const sessionId = '${sessionId}';
232
+ ${jsTab?.content || ''}
233
+ </script>
234
+ </body>
235
+ </html>
236
+ `
237
+
238
+ const blob = new Blob([previewContent], { type: 'text/html' })
239
+ const url = URL.createObjectURL(blob)
240
+ previewRef.current.src = url
241
+ }
242
+
243
+ const handleEditorChange = (value: string | undefined) => {
244
+ if (!value) return
245
+
246
+ setTabs(prev => prev.map(tab =>
247
+ tab.id === activeTab
248
+ ? { ...tab, content: value }
249
+ : tab
250
+ ))
251
+ }
252
+
253
+ const downloadCode = () => {
254
+ const htmlTab = tabs.find(t => t.id === 'html')
255
+ const cssTab = tabs.find(t => t.id === 'css')
256
+ const jsTab = tabs.find(t => t.id === 'js')
257
+
258
+ const fullHTML = `<!DOCTYPE html>
259
+ <html lang="en">
260
+ <head>
261
+ <meta charset="UTF-8">
262
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
263
+ <title>My Reuben OS Project</title>
264
+ <style>
265
+ ${cssTab?.content || ''}
266
+ </style>
267
+ </head>
268
+ <body>
269
+ ${htmlTab?.content.replace(/<.*?html.*?>|<.*?head.*?>|<.*?body.*?>|<\/.*?html.*?>|<\/.*?head.*?>|<\/.*?body.*?>/gi, '').replace(/<link.*?>/g, '').replace(/<script.*?src=.*?><\/script>/g, '') || ''}
270
+ <script>
271
+ ${jsTab?.content || ''}
272
+ </script>
273
+ </body>
274
+ </html>`
275
+
276
+ const blob = new Blob([fullHTML], { type: 'text/html' })
277
+ const url = URL.createObjectURL(blob)
278
+ const a = document.createElement('a')
279
+ a.href = url
280
+ a.download = `webos-project-${sessionId}.html`
281
+ a.click()
282
+ URL.revokeObjectURL(url)
283
+ }
284
+
285
+ const saveToMCP = async () => {
286
+ setIsSaving(true)
287
+ try {
288
+ const response = await fetch('/api/code/save', {
289
+ method: 'POST',
290
+ headers: { 'Content-Type': 'application/json' },
291
+ body: JSON.stringify({
292
+ sessionId,
293
+ code: tabs,
294
+ timestamp: Date.now()
295
+ })
296
+ })
297
+
298
+ if (response.ok) {
299
+ const result = await response.json()
300
+ console.log('Code saved successfully!', result)
301
+
302
+ // Show success message
303
+ const successDiv = document.createElement('div')
304
+ successDiv.className = 'fixed bottom-4 right-4 bg-green-600 text-white px-4 py-2 rounded-lg shadow-lg z-[200]'
305
+ successDiv.textContent = `Saved to: data/vscode_sessions/${sessionId}`
306
+ document.body.appendChild(successDiv)
307
+
308
+ setTimeout(() => {
309
+ successDiv.remove()
310
+ }, 3000)
311
+
312
+ // Also save to localStorage
313
+ localStorage.setItem(`vscode_${sessionId}`, JSON.stringify(tabs))
314
+ }
315
+ } catch (error) {
316
+ console.error('Failed to save code:', error)
317
+
318
+ // Show error message
319
+ const errorDiv = document.createElement('div')
320
+ errorDiv.className = 'fixed bottom-4 right-4 bg-red-600 text-white px-4 py-2 rounded-lg shadow-lg z-[200]'
321
+ errorDiv.textContent = 'Failed to save code'
322
+ document.body.appendChild(errorDiv)
323
+
324
+ setTimeout(() => {
325
+ errorDiv.remove()
326
+ }, 3000)
327
+ }
328
+ setIsSaving(false)
329
+ }
330
+
331
+ const activeTabContent = tabs.find(t => t.id === activeTab)
332
+
333
+ return (
334
+ <Window
335
+ id="vscode"
336
+ title="VS Code - Reuben OS Editor"
337
+ isOpen={true}
338
+ onClose={onClose}
339
+ width={isFullscreen ? window.innerWidth : 1400}
340
+ height={isFullscreen ? window.innerHeight - 32 : 800}
341
+ x={isFullscreen ? 0 : 50}
342
+ y={isFullscreen ? 32 : 50}
343
+ darkMode={true}
344
+ className="vscode-window"
345
+ >
346
+ <div className="flex flex-col h-full bg-[#1e1e1e]">
347
+ {/* Editor Header */}
348
+ <div className="flex items-center justify-between bg-[#2d2d2d] px-4 py-2 border-b border-[#3e3e3e]">
349
+ <div className="flex items-center gap-2">
350
+ <Code size={20} weight="bold" className="text-blue-400" />
351
+ <span className="text-gray-300 text-sm">Session: {sessionId.substring(0, 8)}...</span>
352
+ </div>
353
+
354
+ <div className="flex items-center gap-2">
355
+ <button
356
+ onClick={saveToMCP}
357
+ className="px-3 py-1 bg-green-600 hover:bg-green-700 text-white rounded text-sm flex items-center gap-2"
358
+ disabled={isSaving}
359
+ >
360
+ <FloppyDisk size={16} />
361
+ {isSaving ? 'Saving...' : 'Save'}
362
+ </button>
363
+
364
+ <button
365
+ onClick={downloadCode}
366
+ className="px-3 py-1 bg-blue-600 hover:bg-blue-700 text-white rounded text-sm flex items-center gap-2"
367
+ >
368
+ <Download size={16} />
369
+ Download
370
+ </button>
371
+
372
+ <button
373
+ onClick={() => setShowPreview(!showPreview)}
374
+ className="px-3 py-1 bg-gray-600 hover:bg-gray-700 text-white rounded text-sm flex items-center gap-2"
375
+ >
376
+ {showPreview ? <EyeSlash size={16} /> : <Eye size={16} />}
377
+ Preview
378
+ </button>
379
+
380
+ <button
381
+ onClick={() => setIsFullscreen(!isFullscreen)}
382
+ className="px-3 py-1 bg-gray-600 hover:bg-gray-700 text-white rounded text-sm flex items-center gap-2"
383
+ >
384
+ {isFullscreen ? <ArrowsInSimple size={16} /> : <ArrowsOutSimple size={16} />}
385
+ </button>
386
+ </div>
387
+ </div>
388
+
389
+ {/* Main Content */}
390
+ <div className="flex flex-1 overflow-hidden">
391
+ {/* Editor Section */}
392
+ <div className={`flex flex-col ${showPreview ? 'w-1/2' : 'w-full'} border-r border-[#3e3e3e]`}>
393
+ {/* Tabs */}
394
+ <div className="flex bg-[#252526] border-b border-[#3e3e3e]">
395
+ {tabs.map(tab => (
396
+ <button
397
+ key={tab.id}
398
+ onClick={() => setActiveTab(tab.id)}
399
+ className={`px-4 py-2 text-sm flex items-center gap-2 border-r border-[#3e3e3e] transition-colors ${
400
+ activeTab === tab.id
401
+ ? 'bg-[#1e1e1e] text-white'
402
+ : 'text-gray-400 hover:text-white hover:bg-[#2d2d2d]'
403
+ }`}
404
+ >
405
+ {getTabIcon(tab.id)}
406
+ {tab.name}
407
+ </button>
408
+ ))}
409
+ </div>
410
+
411
+ {/* Monaco Editor */}
412
+ <div className="flex-1">
413
+ <Editor
414
+ height="100%"
415
+ language={activeTabContent?.language}
416
+ value={activeTabContent?.content}
417
+ onChange={handleEditorChange}
418
+ theme="vs-dark"
419
+ options={{
420
+ minimap: { enabled: false },
421
+ fontSize: 14,
422
+ wordWrap: 'on',
423
+ automaticLayout: true,
424
+ scrollBeyondLastLine: false
425
+ }}
426
+ />
427
+ </div>
428
+ </div>
429
+
430
+ {/* Preview Section */}
431
+ {showPreview && (
432
+ <div className="flex-1 flex flex-col bg-white">
433
+ <div className="bg-gray-100 px-4 py-2 border-b border-gray-300 flex items-center justify-between">
434
+ <div className="flex items-center gap-2">
435
+ <Eye size={16} className="text-gray-600" />
436
+ <span className="text-sm font-medium">Live Preview</span>
437
+ </div>
438
+ <button
439
+ onClick={updatePreview}
440
+ className="p-1 hover:bg-gray-200 rounded transition-colors"
441
+ >
442
+ <Play size={16} className="text-gray-600" />
443
+ </button>
444
+ </div>
445
+
446
+ <iframe
447
+ ref={previewRef}
448
+ className="flex-1 w-full bg-white"
449
+ sandbox="allow-scripts"
450
+ title="Code Preview"
451
+ />
452
+ </div>
453
+ )}
454
+ </div>
455
+ </div>
456
+ </Window>
457
+ )
458
+ }
app/components/WebBrowser.tsx ADDED
@@ -0,0 +1,277 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ 'use client'
2
+
3
+ import React, { useState, useRef } from 'react'
4
+ import { Globe, RefreshCw, ArrowLeft, ArrowRight, Home, Lock, ExternalLink, X, Maximize2, Minimize2 } from 'lucide-react'
5
+
6
+ interface Tab {
7
+ id: string
8
+ url: string
9
+ title: string
10
+ isActive: boolean
11
+ }
12
+
13
+ export default function WebBrowser() {
14
+ const [tabs, setTabs] = useState<Tab[]>([
15
+ { id: '1', url: 'https://www.google.com', title: 'Google', isActive: true }
16
+ ])
17
+ const [currentUrl, setCurrentUrl] = useState('https://www.google.com')
18
+ const [inputUrl, setInputUrl] = useState('https://www.google.com')
19
+ const [isLoading, setIsLoading] = useState(false)
20
+ const [isFullscreen, setIsFullscreen] = useState(false)
21
+ const [showBrowser, setShowBrowser] = useState(false)
22
+ const iframeRef = useRef<HTMLIFrameElement>(null)
23
+
24
+ const handleNavigate = (url: string) => {
25
+ // Ensure URL has protocol
26
+ let finalUrl = url
27
+ if (!url.startsWith('http://') && !url.startsWith('https://')) {
28
+ finalUrl = 'https://' + url
29
+ }
30
+
31
+ setCurrentUrl(finalUrl)
32
+ setInputUrl(finalUrl)
33
+ setIsLoading(true)
34
+
35
+ // Update active tab
36
+ setTabs(prevTabs =>
37
+ prevTabs.map(tab =>
38
+ tab.isActive ? { ...tab, url: finalUrl } : tab
39
+ )
40
+ )
41
+ }
42
+
43
+ const handleRefresh = () => {
44
+ if (iframeRef.current) {
45
+ const currentSrc = iframeRef.current.src
46
+ iframeRef.current.src = ''
47
+ setTimeout(() => {
48
+ if (iframeRef.current) {
49
+ iframeRef.current.src = currentSrc
50
+ }
51
+ }, 10)
52
+ }
53
+ }
54
+
55
+ const handleBack = () => {
56
+ // Browser history would be handled by iframe internally
57
+ window.history.back()
58
+ }
59
+
60
+ const handleForward = () => {
61
+ window.history.forward()
62
+ }
63
+
64
+ const handleHome = () => {
65
+ handleNavigate('https://www.google.com')
66
+ }
67
+
68
+ const addNewTab = () => {
69
+ const newTab: Tab = {
70
+ id: Date.now().toString(),
71
+ url: 'https://www.google.com',
72
+ title: 'New Tab',
73
+ isActive: true
74
+ }
75
+
76
+ setTabs(prevTabs => [
77
+ ...prevTabs.map(tab => ({ ...tab, isActive: false })),
78
+ newTab
79
+ ])
80
+ setCurrentUrl(newTab.url)
81
+ setInputUrl(newTab.url)
82
+ }
83
+
84
+ const switchTab = (tabId: string) => {
85
+ const tab = tabs.find(t => t.id === tabId)
86
+ if (tab) {
87
+ setTabs(prevTabs =>
88
+ prevTabs.map(t => ({ ...t, isActive: t.id === tabId }))
89
+ )
90
+ setCurrentUrl(tab.url)
91
+ setInputUrl(tab.url)
92
+ }
93
+ }
94
+
95
+ const closeTab = (tabId: string) => {
96
+ if (tabs.length === 1) return // Don't close last tab
97
+
98
+ const tabIndex = tabs.findIndex(t => t.id === tabId)
99
+ const wasActive = tabs[tabIndex].isActive
100
+
101
+ const newTabs = tabs.filter(t => t.id !== tabId)
102
+
103
+ if (wasActive) {
104
+ const newActiveIndex = tabIndex > 0 ? tabIndex - 1 : 0
105
+ newTabs[newActiveIndex].isActive = true
106
+ setCurrentUrl(newTabs[newActiveIndex].url)
107
+ setInputUrl(newTabs[newActiveIndex].url)
108
+ }
109
+
110
+ setTabs(newTabs)
111
+ }
112
+
113
+ const openInNewWindow = () => {
114
+ window.open(currentUrl, '_blank')
115
+ }
116
+
117
+ if (!showBrowser) {
118
+ return (
119
+ <button
120
+ onClick={() => setShowBrowser(true)}
121
+ className="fixed bottom-4 right-4 z-50 px-4 py-2 bg-blue-600 text-white rounded-lg shadow-lg hover:bg-blue-700 transition-colors flex items-center gap-2"
122
+ >
123
+ <Globe size={20} />
124
+ Open Browser
125
+ </button>
126
+ )
127
+ }
128
+
129
+ return (
130
+ <div className={`fixed ${isFullscreen ? 'inset-0 z-[9999]' : 'inset-4 z-40'} bg-white rounded-lg shadow-2xl flex flex-col transition-all duration-300`}>
131
+ {/* Browser Header */}
132
+ <div className="bg-gray-100 border-b border-gray-300 rounded-t-lg">
133
+ {/* Tabs */}
134
+ <div className="flex items-center bg-gray-200 px-2 py-1 overflow-x-auto">
135
+ {tabs.map(tab => (
136
+ <div
137
+ key={tab.id}
138
+ className={`flex items-center px-3 py-1.5 mr-1 rounded-t-md cursor-pointer transition-colors ${
139
+ tab.isActive ? 'bg-white' : 'bg-gray-100 hover:bg-gray-50'
140
+ }`}
141
+ onClick={() => switchTab(tab.id)}
142
+ >
143
+ <Globe size={14} className="mr-2 text-gray-600" />
144
+ <span className="text-sm max-w-[150px] truncate">{tab.title}</span>
145
+ <button
146
+ onClick={(e) => {
147
+ e.stopPropagation()
148
+ closeTab(tab.id)
149
+ }}
150
+ className="ml-2 p-0.5 hover:bg-gray-200 rounded"
151
+ >
152
+ <X size={14} />
153
+ </button>
154
+ </div>
155
+ ))}
156
+ <button
157
+ onClick={addNewTab}
158
+ className="px-2 py-1 text-gray-600 hover:bg-gray-200 rounded"
159
+ >
160
+ +
161
+ </button>
162
+ </div>
163
+
164
+ {/* Navigation Bar */}
165
+ <div className="flex items-center gap-2 p-2 bg-white">
166
+ <button
167
+ onClick={handleBack}
168
+ className="p-2 hover:bg-gray-100 rounded transition-colors"
169
+ title="Back"
170
+ >
171
+ <ArrowLeft size={18} />
172
+ </button>
173
+ <button
174
+ onClick={handleForward}
175
+ className="p-2 hover:bg-gray-100 rounded transition-colors"
176
+ title="Forward"
177
+ >
178
+ <ArrowRight size={18} />
179
+ </button>
180
+ <button
181
+ onClick={handleRefresh}
182
+ className="p-2 hover:bg-gray-100 rounded transition-colors"
183
+ title="Refresh"
184
+ >
185
+ <RefreshCw size={18} className={isLoading ? 'animate-spin' : ''} />
186
+ </button>
187
+ <button
188
+ onClick={handleHome}
189
+ className="p-2 hover:bg-gray-100 rounded transition-colors"
190
+ title="Home"
191
+ >
192
+ <Home size={18} />
193
+ </button>
194
+
195
+ {/* URL Bar */}
196
+ <div className="flex-1 flex items-center bg-gray-50 rounded-md px-3 py-1.5 border border-gray-300">
197
+ <Lock size={14} className="text-green-600 mr-2" />
198
+ <input
199
+ type="text"
200
+ value={inputUrl}
201
+ onChange={(e) => setInputUrl(e.target.value)}
202
+ onKeyPress={(e) => {
203
+ if (e.key === 'Enter') {
204
+ handleNavigate(inputUrl)
205
+ }
206
+ }}
207
+ className="flex-1 bg-transparent outline-none text-sm"
208
+ placeholder="Enter URL..."
209
+ />
210
+ </div>
211
+
212
+ <button
213
+ onClick={openInNewWindow}
214
+ className="p-2 hover:bg-gray-100 rounded transition-colors"
215
+ title="Open in New Window"
216
+ >
217
+ <ExternalLink size={18} />
218
+ </button>
219
+ <button
220
+ onClick={() => setIsFullscreen(!isFullscreen)}
221
+ className="p-2 hover:bg-gray-100 rounded transition-colors"
222
+ title={isFullscreen ? 'Exit Fullscreen' : 'Fullscreen'}
223
+ >
224
+ {isFullscreen ? <Minimize2 size={18} /> : <Maximize2 size={18} />}
225
+ </button>
226
+ <button
227
+ onClick={() => setShowBrowser(false)}
228
+ className="p-2 hover:bg-gray-100 rounded transition-colors text-red-600"
229
+ title="Close Browser"
230
+ >
231
+ <X size={18} />
232
+ </button>
233
+ </div>
234
+ </div>
235
+
236
+ {/* Browser Content */}
237
+ <div className="flex-1 relative bg-white">
238
+ {/* Loading Indicator */}
239
+ {isLoading && (
240
+ <div className="absolute top-0 left-0 w-full h-1 bg-blue-600 animate-pulse z-10" />
241
+ )}
242
+
243
+ {/* iframe Content */}
244
+ <iframe
245
+ ref={iframeRef}
246
+ src={currentUrl}
247
+ className="w-full h-full border-0"
248
+ onLoad={() => {
249
+ setIsLoading(false)
250
+ // Try to get the title from the iframe (may be blocked by CORS)
251
+ try {
252
+ const title = iframeRef.current?.contentDocument?.title || 'Web Page'
253
+ setTabs(prevTabs =>
254
+ prevTabs.map(tab =>
255
+ tab.isActive ? { ...tab, title } : tab
256
+ )
257
+ )
258
+ } catch (e) {
259
+ // CORS will block this for most external sites
260
+ console.log('Could not access iframe title due to CORS')
261
+ }
262
+ }}
263
+ sandbox="allow-scripts allow-same-origin allow-forms allow-popups"
264
+ title="Web Browser"
265
+ />
266
+
267
+ {/* CORS Notice */}
268
+ <div className="absolute bottom-4 right-4 bg-yellow-100 border border-yellow-400 rounded-lg p-3 max-w-sm">
269
+ <p className="text-xs text-yellow-800">
270
+ <strong>Note:</strong> Some websites may not display due to security restrictions (CORS).
271
+ For full browsing, consider using a proxy service or browser extension.
272
+ </p>
273
+ </div>
274
+ </div>
275
+ </div>
276
+ )
277
+ }
app/components/WebBrowserApp.tsx ADDED
@@ -0,0 +1,438 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ 'use client'
2
+
3
+ import React, { useState, useRef } from 'react'
4
+ import { Globe, RefreshCw, ArrowLeft, ArrowRight, Home, Lock, ExternalLink, AlertTriangle, Shield, X } from 'lucide-react'
5
+ import Window from './Window'
6
+
7
+ interface Tab {
8
+ id: string
9
+ url: string
10
+ title: string
11
+ isActive: boolean
12
+ }
13
+
14
+ interface WebBrowserAppProps {
15
+ onClose: () => void
16
+ }
17
+
18
+ export default function WebBrowserApp({ onClose }: WebBrowserAppProps) {
19
+ const [tabs, setTabs] = useState<Tab[]>([
20
+ { id: '1', url: 'https://www.google.com/webhp?igu=1', title: 'Google', isActive: true }
21
+ ])
22
+ const [currentUrl, setCurrentUrl] = useState('https://www.google.com/webhp?igu=1')
23
+ const [inputUrl, setInputUrl] = useState('https://www.google.com')
24
+ const [isLoading, setIsLoading] = useState(false)
25
+ const [isMaximized, setIsMaximized] = useState(false)
26
+ const [useProxy, setUseProxy] = useState(false)
27
+ const [showSettings, setShowSettings] = useState(false)
28
+ const [showHomePage, setShowHomePage] = useState(true)
29
+ const iframeRef = useRef<HTMLIFrameElement>(null)
30
+
31
+ // List of sites that work well in iframes
32
+ const iframeFriendlySites = [
33
+ { name: 'Google', url: 'https://www.google.com/webhp?igu=1' },
34
+ { name: 'Wikipedia', url: 'https://www.wikipedia.org' },
35
+ { name: 'MDN Web Docs', url: 'https://developer.mozilla.org' },
36
+ { name: 'Stack Overflow', url: 'https://stackoverflow.com' },
37
+ { name: 'GitHub', url: 'https://github.com' },
38
+ { name: 'DuckDuckGo', url: 'https://duckduckgo.com' }
39
+ ]
40
+
41
+ const getProxiedUrl = (url: string) => {
42
+ // Use a CORS proxy service - you can replace this with your own proxy
43
+ if (useProxy) {
44
+ // Option 1: Use allorigins proxy (free, public) - SLOW but works
45
+ return `https://api.allorigins.win/raw?url=${encodeURIComponent(url)}`
46
+
47
+ // Option 2: Use cors-anywhere (may require your own deployment)
48
+ // return `https://cors-anywhere.herokuapp.com/${url}`
49
+
50
+ // Option 3: Use your own backend proxy endpoint
51
+ // return `/api/proxy?url=${encodeURIComponent(url)}`
52
+ }
53
+ return url
54
+ }
55
+
56
+ const handleNavigate = (url: string) => {
57
+ // Ensure URL has protocol
58
+ let finalUrl = url
59
+ if (!url.startsWith('http://') && !url.startsWith('https://')) {
60
+ // Check if it looks like a search query
61
+ if (!url.includes('.') || url.includes(' ')) {
62
+ finalUrl = `https://www.google.com/search?q=${encodeURIComponent(url)}&igu=1`
63
+ } else {
64
+ finalUrl = 'https://' + url
65
+ }
66
+ }
67
+
68
+ setShowHomePage(false)
69
+ const proxiedUrl = getProxiedUrl(finalUrl)
70
+ setCurrentUrl(proxiedUrl)
71
+ setInputUrl(finalUrl)
72
+ setIsLoading(true)
73
+
74
+ // Update active tab
75
+ setTabs(prevTabs =>
76
+ prevTabs.map(tab =>
77
+ tab.isActive ? { ...tab, url: finalUrl, title: new URL(finalUrl).hostname } : tab
78
+ )
79
+ )
80
+ }
81
+
82
+ const handleRefresh = () => {
83
+ if (iframeRef.current) {
84
+ setIsLoading(true)
85
+ const currentSrc = iframeRef.current.src
86
+ iframeRef.current.src = ''
87
+ setTimeout(() => {
88
+ if (iframeRef.current) {
89
+ iframeRef.current.src = currentSrc
90
+ }
91
+ }, 10)
92
+ }
93
+ }
94
+
95
+ const handleBack = () => {
96
+ window.history.back()
97
+ }
98
+
99
+ const handleForward = () => {
100
+ window.history.forward()
101
+ }
102
+
103
+ const handleHome = () => {
104
+ setShowHomePage(true)
105
+ setCurrentUrl('')
106
+ setInputUrl('')
107
+ }
108
+
109
+ const addNewTab = () => {
110
+ const newTab: Tab = {
111
+ id: Date.now().toString(),
112
+ url: 'https://www.google.com/webhp?igu=1',
113
+ title: 'New Tab',
114
+ isActive: true
115
+ }
116
+
117
+ setTabs(prevTabs => [
118
+ ...prevTabs.map(tab => ({ ...tab, isActive: false })),
119
+ newTab
120
+ ])
121
+ handleNavigate(newTab.url)
122
+ }
123
+
124
+ const switchTab = (tabId: string) => {
125
+ const tab = tabs.find(t => t.id === tabId)
126
+ if (tab) {
127
+ setTabs(prevTabs =>
128
+ prevTabs.map(t => ({ ...t, isActive: t.id === tabId }))
129
+ )
130
+ handleNavigate(tab.url)
131
+ }
132
+ }
133
+
134
+ const closeTab = (tabId: string, e: React.MouseEvent) => {
135
+ e.stopPropagation()
136
+ if (tabs.length === 1) return
137
+
138
+ const tabIndex = tabs.findIndex(t => t.id === tabId)
139
+ const wasActive = tabs[tabIndex].isActive
140
+
141
+ const newTabs = tabs.filter(t => t.id !== tabId)
142
+
143
+ if (wasActive) {
144
+ const newActiveIndex = tabIndex > 0 ? tabIndex - 1 : 0
145
+ newTabs[newActiveIndex].isActive = true
146
+ handleNavigate(newTabs[newActiveIndex].url)
147
+ }
148
+
149
+ setTabs(newTabs)
150
+ }
151
+
152
+ const openInNewWindow = () => {
153
+ const activeTab = tabs.find(t => t.isActive)
154
+ if (activeTab) {
155
+ window.open(activeTab.url, '_blank')
156
+ }
157
+ }
158
+
159
+ return (
160
+ <Window
161
+ id="browser"
162
+ title="Web Browser"
163
+ isOpen={true}
164
+ onClose={onClose}
165
+ width={1200}
166
+ height={700}
167
+ x={window.innerWidth / 2 - 600}
168
+ y={window.innerHeight / 2 - 350}
169
+ className="browser-window"
170
+ >
171
+ {/* Browser Header */}
172
+ <div className="bg-gray-100 border-b border-gray-300">
173
+ {/* Tabs */}
174
+ <div className="flex items-center bg-gray-200 px-2 py-1 overflow-x-auto">
175
+ {tabs.map(tab => (
176
+ <div
177
+ key={tab.id}
178
+ className={`flex items-center px-3 py-1.5 mr-1 rounded-t-md cursor-pointer transition-colors min-w-[150px] max-w-[200px] ${
179
+ tab.isActive ? 'bg-white' : 'bg-gray-100 hover:bg-gray-50'
180
+ }`}
181
+ onClick={() => switchTab(tab.id)}
182
+ >
183
+ <Globe size={14} className="mr-2 text-gray-600 flex-shrink-0" />
184
+ <span className="text-sm truncate flex-1">{tab.title}</span>
185
+ {tabs.length > 1 && (
186
+ <button
187
+ onClick={(e) => closeTab(tab.id, e)}
188
+ className="ml-2 p-0.5 hover:bg-gray-200 rounded flex-shrink-0"
189
+ >
190
+ <X size={14} />
191
+ </button>
192
+ )}
193
+ </div>
194
+ ))}
195
+ <button
196
+ onClick={addNewTab}
197
+ className="px-2 py-1 text-gray-600 hover:bg-gray-200 rounded"
198
+ >
199
+ +
200
+ </button>
201
+ </div>
202
+
203
+ {/* Navigation Bar */}
204
+ <div className="flex items-center gap-2 p-2 bg-white">
205
+ <button
206
+ onClick={handleBack}
207
+ className="p-2 hover:bg-gray-100 rounded transition-colors"
208
+ title="Back"
209
+ >
210
+ <ArrowLeft size={18} />
211
+ </button>
212
+ <button
213
+ onClick={handleForward}
214
+ className="p-2 hover:bg-gray-100 rounded transition-colors"
215
+ title="Forward"
216
+ >
217
+ <ArrowRight size={18} />
218
+ </button>
219
+ <button
220
+ onClick={handleRefresh}
221
+ className="p-2 hover:bg-gray-100 rounded transition-colors"
222
+ title="Refresh"
223
+ >
224
+ <RefreshCw size={18} className={isLoading ? 'animate-spin' : ''} />
225
+ </button>
226
+ <button
227
+ onClick={handleHome}
228
+ className="p-2 hover:bg-gray-100 rounded transition-colors"
229
+ title="Home"
230
+ >
231
+ <Home size={18} />
232
+ </button>
233
+
234
+ {/* URL Bar */}
235
+ <div className="flex-1 flex items-center bg-gray-50 rounded-md px-3 py-1.5 border border-gray-300">
236
+ {useProxy ? (
237
+ <Shield size={14} className="text-green-600 mr-2" />
238
+ ) : (
239
+ <Lock size={14} className="text-gray-400 mr-2" />
240
+ )}
241
+ <input
242
+ type="text"
243
+ value={inputUrl}
244
+ onChange={(e) => setInputUrl(e.target.value)}
245
+ onKeyPress={(e) => {
246
+ if (e.key === 'Enter') {
247
+ handleNavigate(inputUrl)
248
+ }
249
+ }}
250
+ className="flex-1 bg-transparent outline-none text-sm"
251
+ placeholder="Enter URL or search..."
252
+ />
253
+ </div>
254
+
255
+ <button
256
+ onClick={() => setShowSettings(!showSettings)}
257
+ className={`p-2 rounded transition-colors ${showSettings ? 'bg-blue-100' : 'hover:bg-gray-100'}`}
258
+ title="Settings"
259
+ >
260
+ <Shield size={18} className={useProxy ? 'text-green-600' : 'text-gray-400'} />
261
+ </button>
262
+ <button
263
+ onClick={openInNewWindow}
264
+ className="p-2 hover:bg-gray-100 rounded transition-colors"
265
+ title="Open in New Window"
266
+ >
267
+ <ExternalLink size={18} />
268
+ </button>
269
+ </div>
270
+
271
+ {/* Settings Panel */}
272
+ {showSettings && (
273
+ <div className="px-4 py-3 bg-blue-50 border-b border-blue-200">
274
+ <div className="flex items-center justify-between mb-2">
275
+ <label className="flex items-center gap-2 cursor-pointer">
276
+ <input
277
+ type="checkbox"
278
+ checked={useProxy}
279
+ onChange={(e) => setUseProxy(e.target.checked)}
280
+ className="w-4 h-4"
281
+ />
282
+ <span className="text-sm font-medium">Use CORS Proxy</span>
283
+ <span className="text-xs text-gray-600">(Bypass cross-origin restrictions)</span>
284
+ </label>
285
+ </div>
286
+ <div className="text-xs text-gray-600">
287
+ <p className="mb-1">• Proxy allows accessing most websites but may affect performance</p>
288
+ <p>• Some interactive features may not work through proxy</p>
289
+ </div>
290
+ </div>
291
+ )}
292
+
293
+ {/* Quick Links */}
294
+ <div className="px-4 py-2 bg-gray-50 border-b flex items-center gap-2 overflow-x-auto">
295
+ <span className="text-xs text-gray-600 mr-2">Quick Links:</span>
296
+ {iframeFriendlySites.map(site => (
297
+ <button
298
+ key={site.name}
299
+ onClick={() => handleNavigate(site.url)}
300
+ className="px-3 py-1 text-xs bg-white border border-gray-300 rounded hover:bg-blue-50 hover:border-blue-400 transition-colors whitespace-nowrap"
301
+ >
302
+ {site.name}
303
+ </button>
304
+ ))}
305
+ </div>
306
+ </div>
307
+
308
+ {/* Browser Content */}
309
+ <div className="flex-1 relative bg-white overflow-hidden">
310
+ {/* Loading Indicator */}
311
+ {isLoading && !showHomePage && (
312
+ <div className="absolute top-0 left-0 w-full h-1 bg-blue-600 z-20">
313
+ <div className="h-full bg-blue-400 animate-pulse" />
314
+ </div>
315
+ )}
316
+
317
+ {/* Home Page */}
318
+ {showHomePage ? (
319
+ <div className="w-full h-full flex flex-col items-center justify-center bg-gradient-to-br from-blue-50 to-purple-50 p-8">
320
+ <div className="max-w-2xl w-full space-y-8">
321
+ {/* Browser Logo */}
322
+ <div className="flex flex-col items-center gap-4">
323
+ <div className="w-24 h-24 rounded-full bg-gradient-to-br from-blue-500 to-cyan-500 flex items-center justify-center shadow-lg">
324
+ <Globe size={48} className="text-white" />
325
+ </div>
326
+ <h1 className="text-4xl font-bold text-gray-800">Web Browser</h1>
327
+ <p className="text-gray-600 text-center">Browse the web with speed and simplicity</p>
328
+ </div>
329
+
330
+ {/* Search Box */}
331
+ <div className="w-full">
332
+ <div className="relative">
333
+ <input
334
+ type="text"
335
+ placeholder="Search or enter website address..."
336
+ className="w-full px-6 py-4 text-lg rounded-full border-2 border-gray-300 focus:border-blue-500 focus:outline-none shadow-md"
337
+ onKeyPress={(e) => {
338
+ if (e.key === 'Enter') {
339
+ const value = (e.target as HTMLInputElement).value
340
+ if (value.trim()) {
341
+ handleNavigate(value)
342
+ }
343
+ }
344
+ }}
345
+ />
346
+ <div className="absolute right-4 top-1/2 -translate-y-1/2 text-gray-400">
347
+ Press Enter ↵
348
+ </div>
349
+ </div>
350
+ </div>
351
+
352
+ {/* Quick Access Grid */}
353
+ <div>
354
+ <h2 className="text-sm font-medium text-gray-600 mb-3">Quick Access</h2>
355
+ <div className="grid grid-cols-3 gap-4">
356
+ {iframeFriendlySites.map(site => (
357
+ <button
358
+ key={site.name}
359
+ onClick={() => handleNavigate(site.url)}
360
+ className="p-4 bg-white rounded-lg shadow hover:shadow-md transition-shadow border border-gray-200 hover:border-blue-400 group"
361
+ >
362
+ <div className="flex flex-col items-center gap-2">
363
+ <div className="w-12 h-12 rounded-full bg-gradient-to-br from-blue-100 to-purple-100 flex items-center justify-center group-hover:from-blue-200 group-hover:to-purple-200 transition-colors">
364
+ <Globe size={24} className="text-blue-600" />
365
+ </div>
366
+ <span className="text-sm font-medium text-gray-700">{site.name}</span>
367
+ </div>
368
+ </button>
369
+ ))}
370
+ </div>
371
+ </div>
372
+
373
+ {/* Performance Tip */}
374
+ <div className="bg-blue-100 border border-blue-300 rounded-lg p-4">
375
+ <div className="flex items-start gap-3">
376
+ <Shield size={20} className="text-blue-600 mt-0.5 flex-shrink-0" />
377
+ <div className="text-sm">
378
+ <p className="text-blue-900 font-medium mb-1">⚡ Fast Direct Access</p>
379
+ <p className="text-blue-800">
380
+ For best performance, websites are loaded directly without proxy. Some sites may block iframe access due to security policies.
381
+ </p>
382
+ </div>
383
+ </div>
384
+ </div>
385
+ </div>
386
+ </div>
387
+ ) : (
388
+ <>
389
+ {/* iframe Content */}
390
+ <iframe
391
+ ref={iframeRef}
392
+ src={currentUrl}
393
+ className="w-full h-full border-0"
394
+ onLoad={() => {
395
+ setIsLoading(false)
396
+ // Try to get the title (may be blocked by CORS)
397
+ try {
398
+ if (iframeRef.current?.contentDocument?.title) {
399
+ const title = iframeRef.current.contentDocument.title
400
+ setTabs(prevTabs =>
401
+ prevTabs.map(tab =>
402
+ tab.isActive ? { ...tab, title } : tab
403
+ )
404
+ )
405
+ }
406
+ } catch (e) {
407
+ // Expected for cross-origin content
408
+ }
409
+ }}
410
+ onError={() => {
411
+ setIsLoading(false)
412
+ }}
413
+ sandbox="allow-scripts allow-same-origin allow-forms allow-popups allow-popups-to-escape-sandbox"
414
+ title="Web Browser"
415
+ />
416
+
417
+ {/* Info Panel - only show if using proxy */}
418
+ {useProxy && (
419
+ <div className="absolute bottom-4 right-4 bg-yellow-100 border border-yellow-400 rounded-lg p-3 max-w-sm z-10">
420
+ <div className="flex items-start gap-2">
421
+ <AlertTriangle size={16} className="text-yellow-700 mt-0.5" />
422
+ <div>
423
+ <p className="text-xs text-yellow-800 font-medium mb-1">
424
+ Using Proxy (Slower)
425
+ </p>
426
+ <p className="text-xs text-yellow-700">
427
+ Proxy adds extra latency. Disable in settings for faster loading.
428
+ </p>
429
+ </div>
430
+ </div>
431
+ </div>
432
+ )}
433
+ </>
434
+ )}
435
+ </div>
436
+ </Window>
437
+ )
438
+ }
app/components/Window.tsx ADDED
@@ -0,0 +1,132 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ 'use client';
2
+
3
+ import React, { ReactNode } from 'react';
4
+ import { Rnd } from 'react-rnd';
5
+
6
+ interface WindowProps {
7
+ id: string;
8
+ title: string;
9
+ isOpen: boolean;
10
+ onClose: () => void;
11
+ onMinimize?: () => void;
12
+ onMaximize?: () => void;
13
+ children: ReactNode;
14
+ width?: number | string;
15
+ height?: number | string;
16
+ x?: number;
17
+ y?: number;
18
+ resizable?: boolean;
19
+ className?: string;
20
+ headerClassName?: string;
21
+ darkMode?: boolean;
22
+ }
23
+
24
+ const Window: React.FC<WindowProps> = ({
25
+ id,
26
+ title,
27
+ isOpen,
28
+ onClose,
29
+ onMinimize,
30
+ onMaximize,
31
+ children,
32
+ width = 800,
33
+ height = 600,
34
+ x = 100,
35
+ y = 100,
36
+ resizable = true,
37
+ className = '',
38
+ headerClassName = '',
39
+ darkMode = false,
40
+ }) => {
41
+ const [isMaximized, setIsMaximized] = React.useState(false);
42
+ const [previousSize, setPreviousSize] = React.useState({ width, height, x, y });
43
+ const [currentPosition, setCurrentPosition] = React.useState({ x, y });
44
+ const [currentSize, setCurrentSize] = React.useState({ width, height });
45
+
46
+ if (!isOpen) return null;
47
+
48
+ const handleMaximize = () => {
49
+ if (!onMaximize) return;
50
+
51
+ if (!isMaximized) {
52
+ setPreviousSize({ width, height, x, y });
53
+ setIsMaximized(true);
54
+ } else {
55
+ setIsMaximized(false);
56
+ }
57
+ onMaximize();
58
+ };
59
+
60
+ const windowClass = darkMode ? 'bg-gray-900 border-gray-700' : 'bg-[#f5f5f5] border-gray-300/50';
61
+ const headerClass = darkMode ? 'bg-gray-800 border-gray-700' : 'bg-[#f1f1f1] border-gray-300';
62
+
63
+ if (isMaximized) {
64
+ return (
65
+ <div
66
+ className={`fixed inset-0 top-8 z-50 flex flex-col overflow-hidden ${windowClass} ${className}`}
67
+ >
68
+ <div
69
+ className={`h-12 flex items-center px-4 space-x-4 border-b ${headerClass} ${headerClassName}`}
70
+ >
71
+ <div className="flex space-x-2">
72
+ <div className="traffic-light traffic-close" onClick={onClose} />
73
+ <div className="traffic-light traffic-min" onClick={onMinimize} />
74
+ <div className="traffic-light traffic-max" onClick={handleMaximize} />
75
+ </div>
76
+ <span className="font-semibold text-gray-700 flex-1 text-center pr-16">{title}</span>
77
+ </div>
78
+ <div className="flex-1 overflow-auto">{children}</div>
79
+ </div>
80
+ );
81
+ }
82
+
83
+ return (
84
+ <Rnd
85
+ default={{
86
+ x: currentPosition.x,
87
+ y: currentPosition.y,
88
+ width: typeof currentSize.width === 'number' ? currentSize.width : 800,
89
+ height: typeof currentSize.height === 'number' ? currentSize.height : 600,
90
+ }}
91
+ position={undefined}
92
+ size={undefined}
93
+ minWidth={400}
94
+ minHeight={300}
95
+ bounds="parent"
96
+ dragHandleClassName="window-drag-handle"
97
+ enableResizing={resizable}
98
+ onDragStop={(e, d) => {
99
+ setCurrentPosition({ x: d.x, y: d.y });
100
+ }}
101
+ onResizeStop={(e, direction, ref, delta, position) => {
102
+ setCurrentSize({
103
+ width: ref.offsetWidth,
104
+ height: ref.offsetHeight,
105
+ });
106
+ setCurrentPosition(position);
107
+ }}
108
+ className="window-transition"
109
+ style={{ zIndex: 50 }}
110
+ >
111
+ <div
112
+ className={`h-full rounded-xl shadow-2xl overflow-hidden border ${windowClass} ${className}`}
113
+ >
114
+ <div
115
+ className={`window-drag-handle h-12 flex items-center px-4 space-x-4 border-b cursor-move ${headerClass} ${headerClassName}`}
116
+ >
117
+ <div className="flex space-x-2">
118
+ <div className="traffic-light traffic-close" onClick={onClose} />
119
+ <div className="traffic-light traffic-min" onClick={onMinimize} />
120
+ <div className="traffic-light traffic-max" onClick={handleMaximize} />
121
+ </div>
122
+ <span className="font-semibold text-gray-700 flex-1 text-center pr-16">{title}</span>
123
+ </div>
124
+ <div className="flex-1 overflow-auto bg-white" style={{ height: 'calc(100% - 3rem)' }}>
125
+ {children}
126
+ </div>
127
+ </div>
128
+ </Rnd>
129
+ );
130
+ };
131
+
132
+ export default Window;
app/gemini/page.tsx ADDED
@@ -0,0 +1,426 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ 'use client'
2
+
3
+ import React, { useState, useRef, useEffect } from 'react'
4
+ import { Upload, Mic, MicOff, Send, Image as ImageIcon, FileText, Loader2, X } from 'lucide-react'
5
+
6
+ interface Message {
7
+ id: string
8
+ role: 'user' | 'assistant'
9
+ content: string
10
+ type: 'text' | 'image' | 'audio'
11
+ imageUrl?: string
12
+ timestamp: Date
13
+ }
14
+
15
+ export default function GeminiAIApp() {
16
+ const [messages, setMessages] = useState<Message[]>([])
17
+ const [inputText, setInputText] = useState('')
18
+ const [isLoading, setIsLoading] = useState(false)
19
+ const [isRecording, setIsRecording] = useState(false)
20
+ const [selectedImage, setSelectedImage] = useState<string | null>(null)
21
+ const [activeTab, setActiveTab] = useState<'chat' | 'transcribe' | 'image'>('chat')
22
+ const [apiKey, setApiKey] = useState('')
23
+ const [showApiKeyInput, setShowApiKeyInput] = useState(true)
24
+
25
+ const fileInputRef = useRef<HTMLInputElement>(null)
26
+ const audioInputRef = useRef<HTMLInputElement>(null)
27
+ const messagesEndRef = useRef<HTMLDivElement>(null)
28
+ const mediaRecorderRef = useRef<MediaRecorder | null>(null)
29
+ const audioChunksRef = useRef<Blob[]>([])
30
+
31
+ useEffect(() => {
32
+ // Check for stored API key
33
+ const storedKey = localStorage.getItem('gemini_api_key')
34
+ if (storedKey) {
35
+ setApiKey(storedKey)
36
+ setShowApiKeyInput(false)
37
+ }
38
+ }, [])
39
+
40
+ useEffect(() => {
41
+ messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' })
42
+ }, [messages])
43
+
44
+ const saveApiKey = () => {
45
+ if (apiKey.trim()) {
46
+ localStorage.setItem('gemini_api_key', apiKey)
47
+ setShowApiKeyInput(false)
48
+ }
49
+ }
50
+
51
+ const handleSendMessage = async () => {
52
+ if (!inputText.trim() && !selectedImage) return
53
+
54
+ const newMessage: Message = {
55
+ id: Date.now().toString(),
56
+ role: 'user',
57
+ content: inputText || 'Image uploaded',
58
+ type: selectedImage ? 'image' : 'text',
59
+ imageUrl: selectedImage || undefined,
60
+ timestamp: new Date()
61
+ }
62
+
63
+ setMessages(prev => [...prev, newMessage])
64
+ setInputText('')
65
+ setSelectedImage(null)
66
+ setIsLoading(true)
67
+
68
+ try {
69
+ const response = await fetch('/api/gemini/chat', {
70
+ method: 'POST',
71
+ headers: { 'Content-Type': 'application/json' },
72
+ body: JSON.stringify({
73
+ message: inputText,
74
+ imageUrl: selectedImage,
75
+ apiKey: apiKey,
76
+ history: messages.slice(-10) // Send last 10 messages for context
77
+ })
78
+ })
79
+
80
+ const data = await response.json()
81
+
82
+ if (data.error) {
83
+ throw new Error(data.error)
84
+ }
85
+
86
+ const aiMessage: Message = {
87
+ id: (Date.now() + 1).toString(),
88
+ role: 'assistant',
89
+ content: data.response,
90
+ type: 'text',
91
+ timestamp: new Date()
92
+ }
93
+
94
+ setMessages(prev => [...prev, aiMessage])
95
+ } catch (error) {
96
+ console.error('Error:', error)
97
+ const errorMessage: Message = {
98
+ id: (Date.now() + 1).toString(),
99
+ role: 'assistant',
100
+ content: `Error: ${error instanceof Error ? error.message : 'Failed to get response'}`,
101
+ type: 'text',
102
+ timestamp: new Date()
103
+ }
104
+ setMessages(prev => [...prev, errorMessage])
105
+ } finally {
106
+ setIsLoading(false)
107
+ }
108
+ }
109
+
110
+ const handleImageUpload = (e: React.ChangeEvent<HTMLInputElement>) => {
111
+ const file = e.target.files?.[0]
112
+ if (file) {
113
+ const reader = new FileReader()
114
+ reader.onloadend = () => {
115
+ setSelectedImage(reader.result as string)
116
+ }
117
+ reader.readAsDataURL(file)
118
+ }
119
+ }
120
+
121
+ const startRecording = async () => {
122
+ try {
123
+ const stream = await navigator.mediaDevices.getUserMedia({ audio: true })
124
+ const mediaRecorder = new MediaRecorder(stream)
125
+ mediaRecorderRef.current = mediaRecorder
126
+ audioChunksRef.current = []
127
+
128
+ mediaRecorder.ondataavailable = (event) => {
129
+ audioChunksRef.current.push(event.data)
130
+ }
131
+
132
+ mediaRecorder.onstop = async () => {
133
+ const audioBlob = new Blob(audioChunksRef.current, { type: 'audio/wav' })
134
+ await transcribeAudio(audioBlob)
135
+ stream.getTracks().forEach(track => track.stop())
136
+ }
137
+
138
+ mediaRecorder.start()
139
+ setIsRecording(true)
140
+ } catch (error) {
141
+ console.error('Error accessing microphone:', error)
142
+ alert('Error accessing microphone. Please check permissions.')
143
+ }
144
+ }
145
+
146
+ const stopRecording = () => {
147
+ if (mediaRecorderRef.current && isRecording) {
148
+ mediaRecorderRef.current.stop()
149
+ setIsRecording(false)
150
+ }
151
+ }
152
+
153
+ const transcribeAudio = async (audioBlob: Blob) => {
154
+ setIsLoading(true)
155
+ const formData = new FormData()
156
+ formData.append('audio', audioBlob, 'recording.wav')
157
+ formData.append('apiKey', apiKey)
158
+
159
+ try {
160
+ const response = await fetch('/api/gemini/transcribe', {
161
+ method: 'POST',
162
+ body: formData
163
+ })
164
+
165
+ const data = await response.json()
166
+
167
+ if (data.error) {
168
+ throw new Error(data.error)
169
+ }
170
+
171
+ setInputText(data.transcription)
172
+ } catch (error) {
173
+ console.error('Transcription error:', error)
174
+ alert('Failed to transcribe audio')
175
+ } finally {
176
+ setIsLoading(false)
177
+ }
178
+ }
179
+
180
+ const handleAudioFileUpload = async (e: React.ChangeEvent<HTMLInputElement>) => {
181
+ const file = e.target.files?.[0]
182
+ if (file) {
183
+ await transcribeAudio(file)
184
+ }
185
+ }
186
+
187
+ return (
188
+ <div className="min-h-screen bg-gradient-to-br from-purple-50 to-blue-50 p-4">
189
+ <div className="max-w-6xl mx-auto">
190
+ <div className="bg-white rounded-2xl shadow-xl overflow-hidden">
191
+ {/* Header */}
192
+ <div className="bg-gradient-to-r from-blue-600 to-purple-600 p-6 text-white">
193
+ <h1 className="text-3xl font-bold mb-2">Gemini AI Assistant</h1>
194
+ <p className="text-blue-100">Chat, Transcribe, and Analyze Images with AI</p>
195
+ </div>
196
+
197
+ {/* API Key Input */}
198
+ {showApiKeyInput && (
199
+ <div className="p-4 bg-yellow-50 border-b border-yellow-200">
200
+ <div className="flex gap-2">
201
+ <input
202
+ type="password"
203
+ placeholder="Enter your Gemini API Key"
204
+ value={apiKey}
205
+ onChange={(e) => setApiKey(e.target.value)}
206
+ className="flex-1 px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
207
+ />
208
+ <button
209
+ onClick={saveApiKey}
210
+ className="px-6 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors"
211
+ >
212
+ Save Key
213
+ </button>
214
+ </div>
215
+ <p className="text-sm text-gray-600 mt-2">
216
+ Get your API key from <a href="https://makersuite.google.com/app/apikey" target="_blank" rel="noopener noreferrer" className="text-blue-600 hover:underline">Google AI Studio</a>
217
+ </p>
218
+ </div>
219
+ )}
220
+
221
+ {/* Tabs */}
222
+ <div className="flex border-b border-gray-200">
223
+ <button
224
+ onClick={() => setActiveTab('chat')}
225
+ className={`flex-1 py-3 px-4 font-medium transition-colors ${
226
+ activeTab === 'chat'
227
+ ? 'bg-blue-50 text-blue-600 border-b-2 border-blue-600'
228
+ : 'text-gray-600 hover:bg-gray-50'
229
+ }`}
230
+ >
231
+ Chat
232
+ </button>
233
+ <button
234
+ onClick={() => setActiveTab('transcribe')}
235
+ className={`flex-1 py-3 px-4 font-medium transition-colors ${
236
+ activeTab === 'transcribe'
237
+ ? 'bg-blue-50 text-blue-600 border-b-2 border-blue-600'
238
+ : 'text-gray-600 hover:bg-gray-50'
239
+ }`}
240
+ >
241
+ Transcribe
242
+ </button>
243
+ <button
244
+ onClick={() => setActiveTab('image')}
245
+ className={`flex-1 py-3 px-4 font-medium transition-colors ${
246
+ activeTab === 'image'
247
+ ? 'bg-blue-50 text-blue-600 border-b-2 border-blue-600'
248
+ : 'text-gray-600 hover:bg-gray-50'
249
+ }`}
250
+ >
251
+ Image Analysis
252
+ </button>
253
+ </div>
254
+
255
+ {/* Chat Messages */}
256
+ <div className="h-[500px] overflow-y-auto p-4 space-y-4">
257
+ {messages.length === 0 && (
258
+ <div className="text-center text-gray-500 mt-20">
259
+ <div className="mb-4">
260
+ {activeTab === 'chat' && <FileText size={48} className="mx-auto text-gray-300" />}
261
+ {activeTab === 'transcribe' && <Mic size={48} className="mx-auto text-gray-300" />}
262
+ {activeTab === 'image' && <ImageIcon size={48} className="mx-auto text-gray-300" />}
263
+ </div>
264
+ <p className="text-lg font-medium">
265
+ {activeTab === 'chat' && 'Start a conversation with Gemini AI'}
266
+ {activeTab === 'transcribe' && 'Record or upload audio to transcribe'}
267
+ {activeTab === 'image' && 'Upload an image for AI analysis'}
268
+ </p>
269
+ </div>
270
+ )}
271
+
272
+ {messages.map((message) => (
273
+ <div
274
+ key={message.id}
275
+ className={`flex ${message.role === 'user' ? 'justify-end' : 'justify-start'}`}
276
+ >
277
+ <div
278
+ className={`max-w-[70%] p-4 rounded-2xl ${
279
+ message.role === 'user'
280
+ ? 'bg-blue-600 text-white'
281
+ : 'bg-gray-100 text-gray-800'
282
+ }`}
283
+ >
284
+ {message.imageUrl && (
285
+ <img
286
+ src={message.imageUrl}
287
+ alt="Uploaded"
288
+ className="mb-2 rounded-lg max-h-64 object-contain"
289
+ />
290
+ )}
291
+ <p className="whitespace-pre-wrap">{message.content}</p>
292
+ <p className={`text-xs mt-2 ${
293
+ message.role === 'user' ? 'text-blue-100' : 'text-gray-500'
294
+ }`}>
295
+ {new Date(message.timestamp).toLocaleTimeString()}
296
+ </p>
297
+ </div>
298
+ </div>
299
+ ))}
300
+
301
+ {isLoading && (
302
+ <div className="flex justify-start">
303
+ <div className="bg-gray-100 p-4 rounded-2xl">
304
+ <Loader2 className="animate-spin h-5 w-5 text-gray-600" />
305
+ </div>
306
+ </div>
307
+ )}
308
+
309
+ <div ref={messagesEndRef} />
310
+ </div>
311
+
312
+ {/* Selected Image Preview */}
313
+ {selectedImage && (
314
+ <div className="px-4 pb-2">
315
+ <div className="relative inline-block">
316
+ <img
317
+ src={selectedImage}
318
+ alt="Selected"
319
+ className="h-20 rounded-lg border-2 border-blue-500"
320
+ />
321
+ <button
322
+ onClick={() => setSelectedImage(null)}
323
+ className="absolute -top-2 -right-2 bg-red-500 text-white rounded-full p-1 hover:bg-red-600"
324
+ >
325
+ <X size={16} />
326
+ </button>
327
+ </div>
328
+ </div>
329
+ )}
330
+
331
+ {/* Input Area */}
332
+ <div className="border-t border-gray-200 p-4">
333
+ <div className="flex gap-2">
334
+ {/* File Inputs */}
335
+ <input
336
+ ref={fileInputRef}
337
+ type="file"
338
+ accept="image/*"
339
+ onChange={handleImageUpload}
340
+ className="hidden"
341
+ />
342
+ <input
343
+ ref={audioInputRef}
344
+ type="file"
345
+ accept="audio/*"
346
+ onChange={handleAudioFileUpload}
347
+ className="hidden"
348
+ />
349
+
350
+ {/* Action Buttons */}
351
+ {activeTab === 'image' && (
352
+ <button
353
+ onClick={() => fileInputRef.current?.click()}
354
+ className="p-3 bg-purple-100 text-purple-600 rounded-lg hover:bg-purple-200 transition-colors"
355
+ title="Upload Image"
356
+ >
357
+ <ImageIcon size={20} />
358
+ </button>
359
+ )}
360
+
361
+ {activeTab === 'transcribe' && (
362
+ <>
363
+ <button
364
+ onClick={isRecording ? stopRecording : startRecording}
365
+ className={`p-3 rounded-lg transition-colors ${
366
+ isRecording
367
+ ? 'bg-red-100 text-red-600 hover:bg-red-200 animate-pulse'
368
+ : 'bg-green-100 text-green-600 hover:bg-green-200'
369
+ }`}
370
+ title={isRecording ? 'Stop Recording' : 'Start Recording'}
371
+ >
372
+ {isRecording ? <MicOff size={20} /> : <Mic size={20} />}
373
+ </button>
374
+ <button
375
+ onClick={() => audioInputRef.current?.click()}
376
+ className="p-3 bg-blue-100 text-blue-600 rounded-lg hover:bg-blue-200 transition-colors"
377
+ title="Upload Audio File"
378
+ >
379
+ <Upload size={20} />
380
+ </button>
381
+ </>
382
+ )}
383
+
384
+ {/* Text Input */}
385
+ <input
386
+ type="text"
387
+ value={inputText}
388
+ onChange={(e) => setInputText(e.target.value)}
389
+ onKeyPress={(e) => e.key === 'Enter' && !e.shiftKey && handleSendMessage()}
390
+ placeholder={
391
+ activeTab === 'chat' ? 'Type your message...' :
392
+ activeTab === 'transcribe' ? 'Transcribed text will appear here...' :
393
+ 'Describe what you want to analyze...'
394
+ }
395
+ className="flex-1 px-4 py-3 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
396
+ disabled={isLoading}
397
+ />
398
+
399
+ {/* Send Button */}
400
+ <button
401
+ onClick={handleSendMessage}
402
+ disabled={isLoading || (!inputText.trim() && !selectedImage)}
403
+ className="px-6 py-3 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors disabled:bg-gray-300 disabled:cursor-not-allowed"
404
+ >
405
+ {isLoading ? <Loader2 className="animate-spin h-5 w-5" /> : <Send size={20} />}
406
+ </button>
407
+ </div>
408
+
409
+ {/* Feature Instructions */}
410
+ <div className="mt-4 text-sm text-gray-600">
411
+ {activeTab === 'chat' && (
412
+ <p>Chat with Gemini AI. Ask questions, get creative responses, or have a conversation.</p>
413
+ )}
414
+ {activeTab === 'transcribe' && (
415
+ <p>Record audio or upload an audio file to transcribe it to text using AI.</p>
416
+ )}
417
+ {activeTab === 'image' && (
418
+ <p>Upload an image to get AI-powered analysis, descriptions, or answers about the image content.</p>
419
+ )}
420
+ </div>
421
+ </div>
422
+ </div>
423
+ </div>
424
+ </div>
425
+ )
426
+ }
app/globals.css CHANGED
@@ -1,26 +1,315 @@
1
  @import "tailwindcss";
 
 
2
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3
  :root {
4
- --background: #ffffff;
5
- --foreground: #171717;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6
  }
7
 
8
  @theme inline {
 
 
 
 
9
  --color-background: var(--background);
10
  --color-foreground: var(--foreground);
11
- --font-sans: var(--font-geist-sans);
12
- --font-mono: var(--font-geist-mono);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
13
  }
14
 
15
- @media (prefers-color-scheme: dark) {
16
- :root {
17
- --background: #0a0a0a;
18
- --foreground: #ededed;
19
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
20
  }
21
 
22
- body {
23
- background: var(--background);
24
- color: var(--foreground);
25
- font-family: Arial, Helvetica, sans-serif;
26
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  @import "tailwindcss";
2
+ @import "tw-animate-css";
3
+ @import "./styles/theme.css";
4
 
5
+ /* Tailwind base layer for compatibility */
6
+ @layer base {
7
+ *,
8
+ ::after,
9
+ ::before,
10
+ ::backdrop,
11
+ ::file-selector-button {
12
+ border-color: var(--color-gray-200, currentColor);
13
+ }
14
+ }
15
+
16
+ /* Custom variant for dark mode */
17
+ @custom-variant dark (&:is(.dark *));
18
+
19
+ /* CSS Variables for theming */
20
  :root {
21
+ --radius: 0.625rem;
22
+ --background: oklch(1 0 0);
23
+ --foreground: oklch(0.145 0 0);
24
+ --card: oklch(1 0 0);
25
+ --card-foreground: oklch(0.145 0 0);
26
+ --popover: oklch(1 0 0);
27
+ --popover-foreground: oklch(0.145 0 0);
28
+ --primary: oklch(0.205 0 0);
29
+ --primary-foreground: oklch(0.985 0 0);
30
+ --secondary: oklch(0.97 0 0);
31
+ --secondary-foreground: oklch(0.205 0 0);
32
+ --muted: oklch(0.97 0 0);
33
+ --muted-foreground: oklch(0.556 0 0);
34
+ --accent: oklch(0.97 0 0);
35
+ --accent-foreground: oklch(0.205 0 0);
36
+ --destructive: oklch(0.577 0.245 27.325);
37
+ --destructive-foreground: oklch(1 0 0);
38
+ --border: oklch(0.922 0 0);
39
+ --input: oklch(0.922 0 0);
40
+ --ring: oklch(0.708 0 0);
41
+ --chart-1: oklch(0.646 0.222 41.116);
42
+ --chart-2: oklch(0.6 0.118 184.704);
43
+ --chart-3: oklch(0.398 0.07 227.392);
44
+ --chart-4: oklch(0.828 0.189 84.429);
45
+ --chart-5: oklch(0.769 0.188 70.08);
46
+ --sidebar: oklch(0.985 0 0);
47
+ --sidebar-foreground: oklch(0.145 0 0);
48
+ --sidebar-primary: oklch(0.205 0 0);
49
+ --sidebar-primary-foreground: oklch(0.985 0 0);
50
+ --sidebar-accent: oklch(0.97 0 0);
51
+ --sidebar-accent-foreground: oklch(0.205 0 0);
52
+ --sidebar-border: oklch(0.922 0 0);
53
+ --sidebar-ring: oklch(0.708 0 0);
54
+ }
55
+
56
+ .dark {
57
+ --background: oklch(0.145 0 0);
58
+ --foreground: oklch(0.985 0 0);
59
+ --card: oklch(0.205 0 0);
60
+ --card-foreground: oklch(0.985 0 0);
61
+ --popover: oklch(0.205 0 0);
62
+ --popover-foreground: oklch(0.985 0 0);
63
+ --primary: oklch(0.922 0 0);
64
+ --primary-foreground: oklch(0.205 0 0);
65
+ --secondary: oklch(0.269 0 0);
66
+ --secondary-foreground: oklch(0.985 0 0);
67
+ --muted: oklch(0.269 0 0);
68
+ --muted-foreground: oklch(0.708 0 0);
69
+ --accent: oklch(0.269 0 0);
70
+ --accent-foreground: oklch(0.985 0 0);
71
+ --destructive: oklch(0.704 0.191 22.216);
72
+ --destructive-foreground: oklch(1 0 0);
73
+ --border: oklch(1 0 0 / 10%);
74
+ --input: oklch(1 0 0 / 15%);
75
+ --ring: oklch(0.556 0 0);
76
+ --chart-1: oklch(0.488 0.243 264.376);
77
+ --chart-2: oklch(0.696 0.17 162.48);
78
+ --chart-3: oklch(0.769 0.188 70.08);
79
+ --chart-4: oklch(0.627 0.265 303.9);
80
+ --chart-5: oklch(0.645 0.246 16.439);
81
+ --sidebar: oklch(0.205 0 0);
82
+ --sidebar-foreground: oklch(0.985 0 0);
83
+ --sidebar-primary: oklch(0.488 0.243 264.376);
84
+ --sidebar-primary-foreground: oklch(0.985 0 0);
85
+ --sidebar-accent: oklch(0.269 0 0);
86
+ --sidebar-accent-foreground: oklch(0.985 0 0);
87
+ --sidebar-border: oklch(1 0 0 / 10%);
88
+ --sidebar-ring: oklch(0.556 0 0);
89
  }
90
 
91
  @theme inline {
92
+ --radius-sm: calc(var(--radius) - 4px);
93
+ --radius-md: calc(var(--radius) - 2px);
94
+ --radius-lg: var(--radius);
95
+ --radius-xl: calc(var(--radius) + 4px);
96
  --color-background: var(--background);
97
  --color-foreground: var(--foreground);
98
+ --color-card: var(--card);
99
+ --color-card-foreground: var(--card-foreground);
100
+ --color-popover: var(--popover);
101
+ --color-popover-foreground: var(--popover-foreground);
102
+ --color-primary: var(--primary);
103
+ --color-primary-foreground: var(--primary-foreground);
104
+ --color-secondary: var(--secondary);
105
+ --color-secondary-foreground: var(--secondary-foreground);
106
+ --color-muted: var(--muted);
107
+ --color-muted-foreground: var(--muted-foreground);
108
+ --color-accent: var(--accent);
109
+ --color-accent-foreground: var(--accent-foreground);
110
+ --color-destructive: var(--destructive);
111
+ --color-destructive-foreground: var(--destructive-foreground);
112
+ --color-border: var(--border);
113
+ --color-input: var(--input);
114
+ --color-ring: var(--ring);
115
+ --color-chart-1: var(--chart-1);
116
+ --color-chart-2: var(--chart-2);
117
+ --color-chart-3: var(--chart-3);
118
+ --color-chart-4: var(--chart-4);
119
+ --color-chart-5: var(--chart-5);
120
+ --color-sidebar: var(--sidebar);
121
+ --color-sidebar-foreground: var(--sidebar-foreground);
122
+ --color-sidebar-primary: var(--sidebar-primary);
123
+ --color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
124
+ --color-sidebar-accent: var(--sidebar-accent);
125
+ --color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
126
+ --color-sidebar-border: var(--sidebar-border);
127
+ --color-sidebar-ring: var(--sidebar-ring);
128
+ --animate-accordion-down: accordion-down 0.2s ease-out;
129
+ --animate-accordion-up: accordion-up 0.2s ease-out;
130
+
131
+ @keyframes accordion-down {
132
+ from {
133
+ height: 0;
134
+ }
135
+ to {
136
+ height: var(--radix-accordion-content-height);
137
+ }
138
+ }
139
+
140
+ @keyframes accordion-up {
141
+ from {
142
+ height: var(--radix-accordion-content-height);
143
+ }
144
+ to {
145
+ height: 0;
146
+ }
147
+ }
148
  }
149
 
150
+ @layer base {
151
+ * {
152
+ @apply border-border outline-ring/50;
 
153
  }
154
+
155
+ body {
156
+ @apply bg-background text-foreground;
157
+ font-family: var(--font-inter, 'Inter'), 'SF Pro Display', -apple-system, BlinkMacSystemFont, 'Ubuntu', ui-sans-serif, system-ui, sans-serif;
158
+ overflow: hidden;
159
+ touch-action: none;
160
+ user-select: none;
161
+ -webkit-font-smoothing: antialiased;
162
+ -moz-osx-font-smoothing: grayscale;
163
+ }
164
+ }
165
+
166
+ /* Sonoma Background */
167
+ .bg-sonoma {
168
+ background: linear-gradient(135deg, #e0c3fc 0%, #8ec5fc 100%);
169
+ background-size: cover;
170
+ background-position: center;
171
+ }
172
+
173
+ /* Glassmorphism utilities */
174
+ .glass {
175
+ background: rgba(255, 255, 255, 0.65);
176
+ backdrop-filter: blur(20px);
177
+ -webkit-backdrop-filter: blur(20px);
178
+ border: 1px solid rgba(255, 255, 255, 0.3);
179
+ }
180
+
181
+ .glass-dark {
182
+ background: rgba(0, 0, 0, 0.5);
183
+ backdrop-filter: blur(20px);
184
+ -webkit-backdrop-filter: blur(20px);
185
+ border: 1px solid rgba(255, 255, 255, 0.1);
186
+ }
187
+
188
+ .dock-glass {
189
+ background: rgba(255, 255, 255, 0.4);
190
+ backdrop-filter: blur(15px);
191
+ -webkit-backdrop-filter: blur(15px);
192
+ border: 1px solid rgba(255, 255, 255, 0.2);
193
+ }
194
+
195
+ /* App Icon Styles */
196
+ .app-icon {
197
+ transition: transform 0.2s cubic-bezier(0.175, 0.885, 0.32, 1.275);
198
+ }
199
+ .app-icon:hover {
200
+ transform: scale(1.2) translateY(-10px);
201
+ }
202
+ .app-icon:active {
203
+ transform: scale(0.95);
204
+ }
205
+
206
+ /* Window Animations */
207
+ .window-transition {
208
+ transition: opacity 0.2s, transform 0.2s;
209
+ }
210
+ .window-hidden {
211
+ opacity: 0;
212
+ transform: scale(0.95);
213
+ pointer-events: none;
214
+ }
215
+
216
+ /* Traffic Lights */
217
+ .traffic-light {
218
+ width: 12px;
219
+ height: 12px;
220
+ border-radius: 50%;
221
+ margin-right: 8px;
222
+ position: relative;
223
+ cursor: pointer;
224
+ transition: all 0.2s ease;
225
+ }
226
+ .traffic-close {
227
+ background-color: #ff5f56;
228
+ border: 1px solid #e0443e;
229
+ }
230
+ .traffic-min {
231
+ background-color: #ffbd2e;
232
+ border: 1px solid #dea123;
233
+ }
234
+ .traffic-max {
235
+ background-color: #27c93f;
236
+ border: 1px solid #1aab29;
237
+ }
238
+
239
+ .traffic-light:hover::after {
240
+ content: '';
241
+ position: absolute;
242
+ top: 0;
243
+ left: 0;
244
+ right: 0;
245
+ bottom: 0;
246
+ display: flex;
247
+ align-items: center;
248
+ justify-content: center;
249
+ font-size: 8px;
250
+ color: rgba(0,0,0,0.5);
251
+ font-weight: bold;
252
+ }
253
+ .traffic-close:hover::after { content: '×'; }
254
+ .traffic-min:hover::after { content: '−'; }
255
+ .traffic-max:hover::after { content: '+'; }
256
+
257
+ /* Custom Scrollbar */
258
+ ::-webkit-scrollbar {
259
+ width: 8px;
260
+ height: 8px;
261
+ }
262
+ ::-webkit-scrollbar-track {
263
+ background: transparent;
264
+ }
265
+ ::-webkit-scrollbar-thumb {
266
+ background: rgba(0,0,0,0.2);
267
+ border-radius: 4px;
268
+ }
269
+ ::-webkit-scrollbar-thumb:hover {
270
+ background: rgba(0,0,0,0.3);
271
  }
272
 
273
+ /* Desktop Icon Grid */
274
+ .desktop-icon {
275
+ @apply flex flex-col items-center gap-1 cursor-pointer transition-all;
 
276
  }
277
+ .desktop-icon:hover {
278
+ transform: scale(1.05);
279
+ }
280
+ .desktop-icon-label {
281
+ @apply text-xs text-white font-medium px-2 py-0.5 rounded;
282
+ background: rgba(0,0,0,0.3);
283
+ text-shadow: 0 1px 2px rgba(0,0,0,0.5);
284
+ }
285
+
286
+ /* macOS style window */
287
+ .macos-window {
288
+ @apply rounded-xl shadow-2xl overflow-hidden;
289
+ background: #f5f5f5;
290
+ border: 1px solid rgba(0, 0, 0, 0.1);
291
+ }
292
+
293
+ .macos-window-header {
294
+ @apply h-10 flex items-center px-4 justify-between;
295
+ background: #f1f1f1;
296
+ border-bottom: 1px solid rgba(0, 0, 0, 0.1);
297
+ }
298
+
299
+ /* Dock styles */
300
+ .dock-container {
301
+ @apply fixed bottom-2 left-0 right-0 flex justify-center z-50;
302
+ }
303
+
304
+ .dock-item {
305
+ @apply relative flex flex-col items-center gap-1;
306
+ }
307
+
308
+ .dock-dot {
309
+ @apply w-1 h-1 rounded-full mt-1 opacity-0 transition-opacity;
310
+ background: rgba(0, 0, 0, 0.5);
311
+ }
312
+
313
+ .dock-item:hover .dock-dot {
314
+ @apply opacity-100;
315
+ }
app/hooks/useKV.ts ADDED
@@ -0,0 +1,37 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ 'use client'
2
+
3
+ import { useState, useEffect } from 'react'
4
+
5
+ // Custom implementation of useKV hook for key-value storage
6
+ export function useKV<T>(key: string, defaultValue: T): [T, (value: T) => void] {
7
+ // Initialize state with the default value
8
+ const [value, setValue] = useState<T>(() => {
9
+ // Try to get the value from localStorage on initial load
10
+ if (typeof window !== 'undefined') {
11
+ try {
12
+ const storedValue = localStorage.getItem(key)
13
+ if (storedValue !== null) {
14
+ return JSON.parse(storedValue)
15
+ }
16
+ } catch (error) {
17
+ console.error(`Error reading from localStorage for key "${key}":`, error)
18
+ }
19
+ }
20
+ return defaultValue
21
+ })
22
+
23
+ // Update localStorage whenever the value changes
24
+ useEffect(() => {
25
+ if (typeof window !== 'undefined') {
26
+ try {
27
+ localStorage.setItem(key, JSON.stringify(value))
28
+ } catch (error) {
29
+ console.error(`Error writing to localStorage for key "${key}":`, error)
30
+ }
31
+ }
32
+ }, [key, value])
33
+
34
+ return [value, setValue]
35
+ }
36
+
37
+ export default useKV