Reubencf commited on
Commit
16a4dbd
·
1 Parent(s): 7661ffa

feat: Complete session management system with automatic session creation

Browse files

- Automatic session creation on page load
- Session persistence with localStorage
- File upload/download with session isolation
- Document generation (DOCX, PDF, PPT, Excel)
- MCP server for Claude Desktop integration
- Removed code executor components

.claude/settings.local.json CHANGED
@@ -3,7 +3,24 @@
3
  "allow": [
4
  "Bash(dir:*)",
5
  "Bash(del nul)",
6
- "Bash(rm:*)"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7
  ],
8
  "deny": [],
9
  "ask": []
 
3
  "allow": [
4
  "Bash(dir:*)",
5
  "Bash(del nul)",
6
+ "Bash(rm:*)",
7
+ "Bash(find:*)",
8
+ "Bash(npm run build:*)",
9
+ "Bash(npm install:*)",
10
+ "Bash(npm run dev:*)",
11
+ "Bash(lsof:*)",
12
+ "Bash(chmod:*)",
13
+ "Bash(node test-mcp-local.js:*)",
14
+ "Bash(REUBENOS_URL=http://localhost:3000 node:*)",
15
+ "Bash(kill:*)",
16
+ "Bash(node test-document-gen.js:*)",
17
+ "Bash(git clone:*)",
18
+ "Bash(cat:*)",
19
+ "Bash(git rm:*)",
20
+ "Bash(git add:*)",
21
+ "Bash(git commit:*)",
22
+ "Bash(git push:*)",
23
+ "Bash(git filter-branch:*)"
24
  ],
25
  "deny": [],
26
  "ask": []
CLAUDE_DESKTOP_SETUP.md CHANGED
@@ -1,99 +1,256 @@
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!
 
1
+ # 🚀 Connect ReubenOS to Claude Desktop via MCP
2
 
3
+ Follow these steps to connect your ReubenOS file management system to Claude Desktop:
4
 
5
+ ## Prerequisites
6
+ 1. Claude Desktop app installed
7
+ 2. Node.js 18+ installed
8
+ 3. ReubenOS running locally
9
+
10
+ ## Step 1: Install MCP Server Dependencies
11
+
12
+ ```bash
13
+ cd /Users/reubenfernandes/Desktop/Mcp-hackathon-winter25
14
+ npm install @modelcontextprotocol/sdk node-fetch
15
  ```
16
 
17
+ ## Step 2: Start ReubenOS (Your Web App)
18
+
19
+ Make sure your ReubenOS is running:
20
+
21
+ ```bash
22
+ npm run dev
23
  ```
24
+
25
+ The server should be running at `http://localhost:3000` (or port 3002 if 3000 is busy)
26
+
27
+ ## Step 3: Configure Claude Desktop
28
+
29
+ ### For macOS:
30
+
31
+ 1. **Open your Claude Desktop configuration file:**
32
+ ```bash
33
+ open ~/Library/Application\ Support/Claude/claude_desktop_config.json
34
+ ```
35
+
36
+ If it doesn't exist, create it:
37
+ ```bash
38
+ mkdir -p ~/Library/Application\ Support/Claude
39
+ touch ~/Library/Application\ Support/Claude/claude_desktop_config.json
40
+ ```
41
+
42
+ 2. **Add this configuration:**
43
+
44
+ ```json
45
+ {
46
+ "mcpServers": {
47
+ "reubenos": {
48
+ "command": "node",
49
+ "args": ["/Users/reubenfernandes/Desktop/Mcp-hackathon-winter25/mcp-server.js"],
50
+ "env": {
51
+ "REUBENOS_URL": "http://localhost:3000"
52
+ }
53
+ }
54
+ }
55
+ }
56
  ```
57
 
58
+ **Note**: If your ReubenOS is running on port 3002, change the URL to `http://localhost:3002`
59
+
60
+ ### For Windows:
61
 
62
+ 1. **Open File Explorer and navigate to:**
63
+ ```
64
+ %APPDATA%\Claude\
65
+ ```
66
+
67
+ 2. **Create or edit `claude_desktop_config.json`:**
68
 
69
  ```json
70
  {
71
  "mcpServers": {
72
+ "reubenos": {
73
+ "command": "node",
74
+ "args": ["C:\\path\\to\\your\\Mcp-hackathon-winter25\\mcp-server.js"],
75
  "env": {
76
+ "REUBENOS_URL": "http://localhost:3000"
 
77
  }
78
  }
79
  }
80
  }
81
  ```
82
 
83
+ ## Step 4: Restart Claude Desktop
84
+
85
+ 1. **Completely quit Claude Desktop** (check system tray/menu bar)
86
+ 2. **Start Claude Desktop again**
87
+ 3. The MCP server should now be connected!
88
+
89
+ ## Step 5: Test the Connection
90
+
91
+ In a new Claude Desktop conversation, try these commands:
92
+
93
+ ### Create a new session:
94
+ ```
95
+ Can you create a new session for me using the reubenos MCP server?
96
+ ```
97
+
98
+ ### Generate a document:
99
+ ```
100
+ Using reubenos, generate a Word document with a sample report and save it to my session
101
+ ```
102
+
103
+ ### List files:
104
+ ```
105
+ Using reubenos, show me all files in my session
106
+ ```
107
 
108
+ ## 📝 Available MCP Tools
 
 
109
 
110
+ Once connected, Claude can use these tools:
111
 
112
+ | Tool | Description | Example Request |
113
+ |------|-------------|-----------------|
114
+ | **create_session** | Create a new isolated session | "Create a new session for me" |
115
+ | **upload_file** | Upload files to session/public | "Upload this text as notes.txt" |
116
+ | **download_file** | Download files from storage | "Download report.docx from my session" |
117
+ | **list_files** | List all files | "Show all files in public folder" |
118
+ | **generate_document** | Create DOCX, PDF, PPT, Excel | "Generate a PowerPoint about AI" |
119
+ | **process_document** | Read and analyze docs | "Read and analyze sales.xlsx" |
120
 
121
+ ## Example Workflows
 
122
 
123
+ ### Document Generation Workflow
124
+ ```
125
+ You: Create a new session for document management
126
+ Claude: [Creates session and provides key]
127
 
128
+ You: Generate a PowerPoint presentation about our Q4 results
129
+ Claude: [Generates PPT and saves to session]
130
 
131
+ You: Now create an Excel spreadsheet with the financial data
132
+ Claude: [Generates Excel file]
133
 
134
+ You: List all my files
135
+ Claude: [Shows all generated documents]
136
+ ```
137
 
138
+ ### File Sharing Workflow
 
 
 
139
  ```
140
+ You: Create a session and upload my notes as a public file
141
+ Claude: [Creates session and uploads to public]
142
 
143
+ You: List all public files
144
+ Claude: [Shows all publicly shared files]
145
+ ```
146
+
147
+ ## 🔧 Troubleshooting
148
+
149
+ ### Issue: MCP Server Not Connecting
150
+
151
+ 1. **Verify ReubenOS is running:**
152
+ ```bash
153
+ curl http://localhost:3000
154
+ ```
155
+
156
+ 2. **Check Claude Desktop logs:**
157
+ - macOS: `~/Library/Logs/Claude/`
158
+ - Windows: `%LOCALAPPDATA%\Claude\logs\`
159
+
160
+ 3. **Verify Node.js version:**
161
+ ```bash
162
+ node --version # Should be 18+
163
+ ```
164
+
165
+ ### Issue: Permission Denied
166
+
167
+ Make the MCP server executable:
168
+ ```bash
169
+ chmod +x /Users/reubenfernandes/Desktop/Mcp-hackathon-winter25/mcp-server.js
170
  ```
171
 
172
+ ### Issue: Port Already in Use
173
+
174
+ Check which port your app is using:
175
+ ```bash
176
+ lsof -i :3000
177
+ lsof -i :3002
178
  ```
179
 
180
+ Update the `REUBENOS_URL` in the config accordingly.
181
+
182
+ ### View Debug Output
183
+
184
+ In Claude Desktop, you can open Developer Tools:
185
+ - macOS: `Cmd+Option+I`
186
+ - Windows: `Ctrl+Shift+I`
187
+
188
+ Check the Console tab for any MCP-related errors.
189
+
190
+ ## 🎯 Quick Test
191
+
192
+ Create this test script as `test-reubenos.sh`:
193
+
194
+ ```bash
195
+ #!/bin/bash
196
+ echo "Testing ReubenOS Connection..."
197
+
198
+ # Check if server is running
199
+ if curl -s http://localhost:3000 > /dev/null 2>&1; then
200
+ echo "✅ ReubenOS running on port 3000"
201
+ PORT=3000
202
+ elif curl -s http://localhost:3002 > /dev/null 2>&1; then
203
+ echo "✅ ReubenOS running on port 3002"
204
+ PORT=3002
205
+ else
206
+ echo "❌ ReubenOS not running. Start with: npm run dev"
207
+ exit 1
208
+ fi
209
+
210
+ # Test session creation
211
+ echo -e "\nTesting session creation..."
212
+ response=$(curl -s -X POST http://localhost:$PORT/api/sessions/create \
213
+ -H "Content-Type: application/json" \
214
+ -d '{"metadata": {"test": true}}')
215
+
216
+ if echo "$response" | grep -q "session"; then
217
+ echo "✅ Session API working!"
218
+ echo "$response" | python3 -m json.tool
219
+ else
220
+ echo "❌ Session API not responding correctly"
221
+ fi
222
+ ```
223
+
224
+ Run it:
225
+ ```bash
226
+ chmod +x test-reubenos.sh
227
+ ./test-reubenos.sh
228
  ```
 
229
 
230
+ ## 📋 What Success Looks Like
231
+
232
+ When properly connected, in Claude Desktop you'll be able to:
233
+
234
+ 1. ✅ See "reubenos" mentioned when asking about available MCP servers
235
+ 2. ✅ Create sessions and get unique session keys
236
+ 3. ✅ Generate Word, PDF, PowerPoint, and Excel documents
237
+ 4. ✅ Upload and download files
238
+ 5. ✅ List files in session and public folders
239
+ 6. ✅ Process and analyze documents
240
+
241
+ ## 💡 Pro Tips
242
 
243
+ 1. **Keep your session key safe** - It's your access to private files
244
+ 2. **Use public folder** for files you want to share
245
+ 3. **Sessions auto-expire** after 24 hours of inactivity
246
+ 4. **Generate multiple documents** in one session for organization
247
 
248
+ ## Need More Help?
 
 
 
 
 
249
 
250
+ - Check the [MCP Documentation](https://github.com/anthropics/model-context-protocol)
251
+ - View your API documentation at `http://localhost:3000/mcp-config.json`
252
+ - Check server logs with `npm run dev`
253
 
254
+ ---
 
 
255
 
256
+ **Ready to go!** Your ReubenOS file management system is now accessible through Claude Desktop! 🎉
Empc-hackathonbackendmcp_server.py DELETED
File without changes
Empc-hackathonpublicbackground_readme.txt DELETED
@@ -1 +0,0 @@
1
- Please add your background.webp file to the public directory at E:\mpc-hackathon\public\background.webp
 
 
RUN_INSTRUCTIONS.md ADDED
@@ -0,0 +1,134 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # 🚀 How to Run ReubenOS with Claude Desktop MCP
2
+
3
+ ## Understanding the Architecture
4
+
5
+ ```
6
+ ┌─────────────────┐ ┌──────────────┐ ┌─────────────┐
7
+ │ Claude Desktop │ ──MCP──> │ MCP Server │ ──API─> │ ReubenOS │
8
+ │ (Auto-starts │ │(mcp-server.js)│ │ (port 3000) │
9
+ │ MCP server) │ └──────────────┘ └─────────────┘
10
+ └─────────────────┘
11
+ ```
12
+
13
+ ## Step-by-Step Instructions
14
+
15
+ ### 1️⃣ Start ReubenOS (Your Web App)
16
+
17
+ ```bash
18
+ cd /Users/reubenfernandes/Desktop/Mcp-hackathon-winter25
19
+ npm run dev
20
+ ```
21
+
22
+ ✅ This starts your web app on `http://localhost:3000`
23
+ ✅ You can access the Session Manager at http://localhost:3000
24
+
25
+ ### 2️⃣ Configure Claude Desktop
26
+
27
+ Create/Edit the config file:
28
+ ```bash
29
+ # Open the config file
30
+ open ~/Library/Application\ Support/Claude/claude_desktop_config.json
31
+ ```
32
+
33
+ Add this configuration:
34
+ ```json
35
+ {
36
+ "mcpServers": {
37
+ "reubenos": {
38
+ "command": "node",
39
+ "args": ["/Users/reubenfernandes/Desktop/Mcp-hackathon-winter25/mcp-server.js"],
40
+ "env": {
41
+ "REUBENOS_URL": "http://localhost:3000"
42
+ }
43
+ }
44
+ }
45
+ }
46
+ ```
47
+
48
+ ### 3️⃣ Restart Claude Desktop
49
+
50
+ 1. Completely quit Claude Desktop (⌘Q or check menu bar)
51
+ 2. Start Claude Desktop again
52
+ 3. **Claude Desktop will automatically start the MCP server!**
53
+
54
+ ## ⚠️ IMPORTANT: You DON'T manually run mcp-server.js!
55
+
56
+ The MCP server (`mcp-server.js`) is **automatically started by Claude Desktop** when it reads your config. You only need to:
57
+ 1. Run your ReubenOS app (`npm run dev`)
58
+ 2. Configure Claude Desktop
59
+ 3. Restart Claude Desktop
60
+
61
+ ## Testing Your Setup
62
+
63
+ ### Test 1: Check if ReubenOS is running
64
+ ```bash
65
+ curl http://localhost:3000
66
+ ```
67
+
68
+ ### Test 2: Test MCP Integration (without Claude Desktop)
69
+ ```bash
70
+ cd /Users/reubenfernandes/Desktop/Mcp-hackathon-winter25
71
+ node test-mcp-local.js
72
+ ```
73
+
74
+ ### Test 3: In Claude Desktop
75
+ Once configured, in a new Claude Desktop chat, try:
76
+ ```
77
+ "Using reubenos, create a new session"
78
+ ```
79
+
80
+ ## Troubleshooting
81
+
82
+ ### If port 3000 is busy:
83
+ ```bash
84
+ # Check what's using port 3000
85
+ lsof -i :3000
86
+
87
+ # Kill the process (replace PID with actual process ID)
88
+ kill -9 PID
89
+
90
+ # Or use a different port
91
+ npm run dev -- --port 3002
92
+ ```
93
+
94
+ Then update your Claude Desktop config:
95
+ ```json
96
+ "REUBENOS_URL": "http://localhost:3002"
97
+ ```
98
+
99
+ ### If MCP doesn't connect in Claude Desktop:
100
+
101
+ 1. Check Claude Desktop logs:
102
+ - Open Developer Tools: `Cmd+Option+I`
103
+ - Check Console for errors
104
+
105
+ 2. Verify config file is valid JSON:
106
+ ```bash
107
+ cat ~/Library/Application\ Support/Claude/claude_desktop_config.json | python3 -m json.tool
108
+ ```
109
+
110
+ 3. Make sure ReubenOS is running:
111
+ ```bash
112
+ ps aux | grep "next dev"
113
+ ```
114
+
115
+ ## Quick Commands
116
+
117
+ ```bash
118
+ # Start everything (run in project directory)
119
+ npm run dev
120
+
121
+ # Test MCP locally (optional, for debugging)
122
+ node test-mcp-local.js
123
+
124
+ # Check if services are running
125
+ curl http://localhost:3000 # Should return HTML
126
+ ```
127
+
128
+ ## Summary
129
+
130
+ ✅ **You only run ONE server manually**: `npm run dev` (ReubenOS)
131
+ ✅ **Claude Desktop runs the MCP server automatically**
132
+ ✅ **The MCP server connects to your ReubenOS API**
133
+
134
+ That's it! Once ReubenOS is running and Claude Desktop is configured, everything works automatically.
SESSION_SYSTEM.md ADDED
@@ -0,0 +1,230 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # 📁 ReubenOS Session & File System
2
+
3
+ ## 🗂️ Folder Structure
4
+
5
+ ```
6
+ data/
7
+ ├── files/ # PRIVATE session folders (requires session key)
8
+ │ ├── session_xxx/
9
+ │ │ ├── session.json
10
+ │ │ ├── document1.docx
11
+ │ │ └── report.pdf
12
+ │ └── session_yyy/
13
+ │ └── ...
14
+ └── public/ # PUBLIC folder (no authentication needed)
15
+ ├── shared-doc.docx
16
+ └── public-file.pdf
17
+ ```
18
+
19
+ ## 🔐 How It Works
20
+
21
+ ### **Private Sessions (data/files/)**
22
+ - Each user gets a **unique session key** when they create a session
23
+ - Files are stored in `data/files/{session_id}/`
24
+ - **Only users with the correct session key can access their files**
25
+ - Complete isolation between users
26
+ - Example: User A cannot see User B's files
27
+
28
+ ### **Public Folder (data/public/)**
29
+ - **No authentication required** to upload or download
30
+ - Anyone can upload files here
31
+ - Files are shared with everyone
32
+ - Use for: shared resources, public documents, collaboration
33
+
34
+ ## 🔑 Session System
35
+
36
+ ### Creating a Session
37
+ ```bash
38
+ POST /api/sessions/create
39
+ Response:
40
+ {
41
+ "success": true,
42
+ "session": {
43
+ "id": "session_1234567890_abc123",
44
+ "key": "e2d51cc7ab787de753ff574d5972a042...",
45
+ "createdAt": "2025-11-14T12:00:00.000Z"
46
+ }
47
+ }
48
+ ```
49
+
50
+ **IMPORTANT**: Save the `key` - you'll need it for all operations!
51
+
52
+ ### Uploading to Private Session
53
+ ```bash
54
+ POST /api/sessions/upload
55
+ Headers:
56
+ x-session-key: YOUR_SESSION_KEY
57
+ Body (form-data):
58
+ file: [your file]
59
+ public: false
60
+ ```
61
+
62
+ ### Uploading to Public Folder
63
+ ```bash
64
+ # Option 1: Via sessions endpoint
65
+ POST /api/sessions/upload
66
+ Body (form-data):
67
+ file: [your file]
68
+ public: true
69
+ # NO session key needed!
70
+
71
+ # Option 2: Direct public endpoint
72
+ POST /api/public/upload
73
+ Body (form-data):
74
+ file: [your file]
75
+ # NO session key needed!
76
+ ```
77
+
78
+ ### Downloading Files
79
+
80
+ **From Private Session:**
81
+ ```bash
82
+ GET /api/sessions/download?file=document.docx
83
+ Headers:
84
+ x-session-key: YOUR_SESSION_KEY
85
+ ```
86
+
87
+ **From Public Folder:**
88
+ ```bash
89
+ GET /api/sessions/download?file=document.docx&public=true
90
+ # NO session key needed!
91
+ ```
92
+
93
+ ## 📄 Document Generation
94
+
95
+ ### Generate Document to Private Session
96
+ ```bash
97
+ POST /api/documents/generate
98
+ Headers:
99
+ x-session-key: YOUR_SESSION_KEY
100
+ Body:
101
+ {
102
+ "type": "docx",
103
+ "fileName": "my-document",
104
+ "content": {
105
+ "title": "My Document",
106
+ "sections": [
107
+ {
108
+ "type": "header",
109
+ "content": "Header text here"
110
+ },
111
+ {
112
+ "type": "body",
113
+ "paragraphs": [
114
+ "Paragraph 1",
115
+ "Paragraph 2"
116
+ ]
117
+ }
118
+ ]
119
+ },
120
+ "isPublic": false
121
+ }
122
+ ```
123
+
124
+ ### Generate Document to Public Folder
125
+ Same as above, but set:
126
+ ```json
127
+ "isPublic": true
128
+ ```
129
+
130
+ ## 🔒 Security Features
131
+
132
+ 1. **Session Isolation**: Each session key gives access ONLY to that session's files
133
+ 2. **Cryptographic Keys**: Session keys are 64-character hex strings (256-bit security)
134
+ 3. **No Cross-Session Access**: Users cannot access other users' private files
135
+ 4. **Public by Choice**: Users explicitly choose to make files public
136
+ 5. **Auto-Cleanup**: Old sessions (24+ hours inactive) are automatically deleted
137
+
138
+ ## 📊 Use Cases
139
+
140
+ ### Private Use (Session-based)
141
+ - Personal documents
142
+ - User-specific data
143
+ - Private reports
144
+ - Confidential files
145
+ - Individual workspace
146
+
147
+ ### Public Use (Shared)
148
+ - Collaboration documents
149
+ - Shared resources
150
+ - Templates
151
+ - Public announcements
152
+ - Common files
153
+
154
+ ## 💡 Best Practices
155
+
156
+ 1. **Keep Your Session Key Secure**
157
+ - Don't share your session key
158
+ - Store it securely (like a password)
159
+ - Each user should have their own session
160
+
161
+ 2. **Use Public Folder Wisely**
162
+ - Only upload files meant to be shared
163
+ - Don't upload sensitive information
164
+ - Remember: anyone can download from public
165
+
166
+ 3. **Session Management**
167
+ - Create a new session for each user
168
+ - Sessions expire after 24 hours of inactivity
169
+ - Re-create session if expired
170
+
171
+ 4. **File Naming**
172
+ - Use descriptive file names
173
+ - Avoid duplicate names in same folder
174
+ - Include file extensions
175
+
176
+ ## 🧪 Testing Your Setup
177
+
178
+ ```bash
179
+ # 1. Create a session
180
+ curl -X POST http://localhost:3000/api/sessions/create \
181
+ -H "Content-Type: application/json" \
182
+ -d '{"metadata": {"user": "test"}}'
183
+
184
+ # Save the session key from response!
185
+
186
+ # 2. Generate a document (private)
187
+ curl -X POST http://localhost:3000/api/documents/generate \
188
+ -H "Content-Type: application/json" \
189
+ -H "x-session-key: YOUR_SESSION_KEY" \
190
+ -d '{
191
+ "type": "docx",
192
+ "fileName": "test-doc",
193
+ "content": {
194
+ "title": "Test Document",
195
+ "content": "This is a test document."
196
+ },
197
+ "isPublic": false
198
+ }'
199
+
200
+ # 3. List your files
201
+ curl http://localhost:3000/api/sessions/files \
202
+ -H "x-session-key: YOUR_SESSION_KEY"
203
+
204
+ # 4. Upload to public (no auth needed!)
205
+ curl -X POST http://localhost:3000/api/public/upload \
206
207
+ ```
208
+
209
+ ## ❓ FAQ
210
+
211
+ **Q: Can I access another user's files?**
212
+ A: No. Each session is completely isolated. You need the exact session key to access a session's files.
213
+
214
+ **Q: What happens if I lose my session key?**
215
+ A: Your files become inaccessible. Session keys cannot be recovered. Always save your session key securely!
216
+
217
+ **Q: Can I make a private file public later?**
218
+ A: Yes, download it from your session and re-upload to public folder.
219
+
220
+ **Q: How long do sessions last?**
221
+ A: Sessions are automatically cleaned up after 24 hours of inactivity.
222
+
223
+ **Q: Where are files actually stored?**
224
+ A:
225
+ - Private: `data/files/{session_id}/`
226
+ - Public: `data/public/`
227
+
228
+ ---
229
+
230
+ **Security Note**: Never share your session key. Treat it like a password!
app/api/code/execute/route.ts DELETED
@@ -1,186 +0,0 @@
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 DELETED
@@ -1,85 +0,0 @@
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 DELETED
@@ -1,130 +0,0 @@
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/documents/generate/route.ts ADDED
@@ -0,0 +1,194 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { NextRequest, NextResponse } from 'next/server';
2
+ import { SessionManager } from '@/lib/sessionManager';
3
+ import { DocumentGenerator } from '@/lib/documentGenerators';
4
+
5
+ export async function POST(request: NextRequest) {
6
+ try {
7
+ const sessionManager = SessionManager.getInstance();
8
+
9
+ // Get session key from headers
10
+ const sessionKey = request.headers.get('x-session-key');
11
+ if (!sessionKey) {
12
+ return NextResponse.json(
13
+ { success: false, error: 'Session key is required' },
14
+ { status: 401 }
15
+ );
16
+ }
17
+
18
+ // Validate session
19
+ const isValid = await sessionManager.validateSession(sessionKey);
20
+ if (!isValid) {
21
+ return NextResponse.json(
22
+ { success: false, error: 'Invalid or expired session key' },
23
+ { status: 401 }
24
+ );
25
+ }
26
+
27
+ const body = await request.json();
28
+ const { type, fileName, content, isPublic = false } = body;
29
+
30
+ if (!type || !fileName || !content) {
31
+ return NextResponse.json(
32
+ { success: false, error: 'Type, fileName, and content are required' },
33
+ { status: 400 }
34
+ );
35
+ }
36
+
37
+ let fileBuffer: Buffer;
38
+ let finalFileName = fileName;
39
+
40
+ try {
41
+ switch (type.toLowerCase()) {
42
+ case 'docx':
43
+ case 'word':
44
+ fileBuffer = await DocumentGenerator.generateDOCX({
45
+ title: content.title,
46
+ content: content.body || content.content || content
47
+ });
48
+ if (!finalFileName.endsWith('.docx')) {
49
+ finalFileName += '.docx';
50
+ }
51
+ break;
52
+
53
+ case 'pdf':
54
+ fileBuffer = await DocumentGenerator.generatePDF({
55
+ title: content.title,
56
+ content: content.body || content.content || content
57
+ });
58
+ if (!finalFileName.endsWith('.pdf')) {
59
+ finalFileName += '.pdf';
60
+ }
61
+ break;
62
+
63
+ case 'latex':
64
+ case 'latex-pdf':
65
+ fileBuffer = await DocumentGenerator.generateLatexPDF(
66
+ content.latex || content.body || content.content || content
67
+ );
68
+ if (!finalFileName.endsWith('.pdf')) {
69
+ finalFileName += '.pdf';
70
+ }
71
+ break;
72
+
73
+ case 'ppt':
74
+ case 'pptx':
75
+ case 'powerpoint':
76
+ const slides = content.slides || [
77
+ {
78
+ title: content.title || 'Presentation',
79
+ content: content.body || content.content || '',
80
+ bullets: content.bullets
81
+ }
82
+ ];
83
+ fileBuffer = await DocumentGenerator.generatePowerPoint(slides);
84
+ if (!finalFileName.endsWith('.pptx')) {
85
+ finalFileName += '.pptx';
86
+ }
87
+ break;
88
+
89
+ case 'excel':
90
+ case 'xlsx':
91
+ case 'spreadsheet':
92
+ const excelData = content.sheets || {
93
+ sheets: [{
94
+ name: content.sheetName || 'Sheet1',
95
+ data: {
96
+ headers: content.headers || [],
97
+ rows: content.rows || []
98
+ }
99
+ }]
100
+ };
101
+ fileBuffer = await DocumentGenerator.generateExcel(excelData);
102
+ if (!finalFileName.endsWith('.xlsx')) {
103
+ finalFileName += '.xlsx';
104
+ }
105
+ break;
106
+
107
+ default:
108
+ return NextResponse.json(
109
+ { success: false, error: `Unsupported document type: ${type}` },
110
+ { status: 400 }
111
+ );
112
+ }
113
+ } catch (error) {
114
+ console.error('Error generating document:', error);
115
+ return NextResponse.json(
116
+ { success: false, error: `Failed to generate ${type} document: ${error}` },
117
+ { status: 500 }
118
+ );
119
+ }
120
+
121
+ // Save the generated file
122
+ let filePath: string;
123
+ if (isPublic) {
124
+ filePath = await sessionManager.saveFileToPublic(finalFileName, fileBuffer);
125
+ } else {
126
+ filePath = await sessionManager.saveFileToSession(sessionKey, finalFileName, fileBuffer);
127
+ }
128
+
129
+ return NextResponse.json({
130
+ success: true,
131
+ message: `${type.toUpperCase()} document generated successfully`,
132
+ fileName: finalFileName,
133
+ size: fileBuffer.length,
134
+ type: type,
135
+ isPublic,
136
+ path: filePath
137
+ });
138
+ } catch (error) {
139
+ console.error('Error in document generation:', error);
140
+ return NextResponse.json(
141
+ { success: false, error: 'Failed to generate document' },
142
+ { status: 500 }
143
+ );
144
+ }
145
+ }
146
+
147
+ export async function GET() {
148
+ return NextResponse.json({
149
+ message: 'Document generation endpoint',
150
+ endpoint: '/api/documents/generate',
151
+ method: 'POST',
152
+ headers: {
153
+ 'x-session-key': 'Your session key (required)'
154
+ },
155
+ body: {
156
+ type: 'Document type: docx, pdf, latex, ppt, excel',
157
+ fileName: 'Output file name',
158
+ isPublic: 'true/false - whether to save in public folder',
159
+ content: {
160
+ description: 'Content structure varies by type',
161
+ examples: {
162
+ docx: {
163
+ title: 'Document Title',
164
+ content: 'Document content with markdown formatting'
165
+ },
166
+ pdf: {
167
+ title: 'PDF Title',
168
+ content: 'PDF content with markdown formatting'
169
+ },
170
+ powerpoint: {
171
+ slides: [
172
+ {
173
+ title: 'Slide 1',
174
+ content: 'Content',
175
+ bullets: ['Point 1', 'Point 2']
176
+ }
177
+ ]
178
+ },
179
+ excel: {
180
+ sheets: [
181
+ {
182
+ name: 'Sheet1',
183
+ data: {
184
+ headers: ['Column 1', 'Column 2'],
185
+ rows: [['Data 1', 'Data 2']]
186
+ }
187
+ }
188
+ ]
189
+ }
190
+ }
191
+ }
192
+ }
193
+ });
194
+ }
app/api/documents/process/route.ts ADDED
@@ -0,0 +1,216 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { NextRequest, NextResponse } from 'next/server';
2
+ import { SessionManager } from '@/lib/sessionManager';
3
+ import mammoth from 'mammoth';
4
+ import ExcelJS from 'exceljs';
5
+ import fs from 'fs/promises';
6
+ import path from 'path';
7
+
8
+ export async function POST(request: NextRequest) {
9
+ try {
10
+ const sessionManager = SessionManager.getInstance();
11
+
12
+ // Get session key from headers
13
+ const sessionKey = request.headers.get('x-session-key');
14
+ if (!sessionKey) {
15
+ return NextResponse.json(
16
+ { success: false, error: 'Session key is required' },
17
+ { status: 401 }
18
+ );
19
+ }
20
+
21
+ // Validate session
22
+ const isValid = await sessionManager.validateSession(sessionKey);
23
+ if (!isValid) {
24
+ return NextResponse.json(
25
+ { success: false, error: 'Invalid or expired session key' },
26
+ { status: 401 }
27
+ );
28
+ }
29
+
30
+ const body = await request.json();
31
+ const { fileName, isPublic = false, operation = 'read' } = body;
32
+
33
+ if (!fileName) {
34
+ return NextResponse.json(
35
+ { success: false, error: 'File name is required' },
36
+ { status: 400 }
37
+ );
38
+ }
39
+
40
+ // Get file buffer
41
+ let fileBuffer: Buffer;
42
+ try {
43
+ if (isPublic) {
44
+ fileBuffer = await sessionManager.getFileFromPublic(fileName);
45
+ } else {
46
+ fileBuffer = await sessionManager.getFileFromSession(sessionKey, fileName);
47
+ }
48
+ } catch (error) {
49
+ return NextResponse.json(
50
+ { success: false, error: 'File not found' },
51
+ { status: 404 }
52
+ );
53
+ }
54
+
55
+ const ext = fileName.split('.').pop()?.toLowerCase();
56
+ let content: any = {};
57
+
58
+ switch (ext) {
59
+ case 'docx':
60
+ // Process Word document
61
+ try {
62
+ const result = await mammoth.extractRawText({ buffer: fileBuffer });
63
+ content = {
64
+ type: 'docx',
65
+ text: result.value,
66
+ messages: result.messages
67
+ };
68
+
69
+ // Also get structured HTML
70
+ const htmlResult = await mammoth.convertToHtml({ buffer: fileBuffer });
71
+ content.html = htmlResult.value;
72
+ } catch (error) {
73
+ content = {
74
+ type: 'docx',
75
+ error: 'Failed to process Word document',
76
+ details: error
77
+ };
78
+ }
79
+ break;
80
+
81
+ case 'xlsx':
82
+ case 'xls':
83
+ // Process Excel spreadsheet
84
+ try {
85
+ const workbook = new ExcelJS.Workbook();
86
+ await workbook.xlsx.load(fileBuffer as any);
87
+
88
+ const sheets: any[] = [];
89
+ workbook.eachSheet((worksheet) => {
90
+ const sheetData: any = {
91
+ name: worksheet.name,
92
+ rowCount: worksheet.rowCount,
93
+ columnCount: worksheet.columnCount,
94
+ data: []
95
+ };
96
+
97
+ worksheet.eachRow((row, rowNumber) => {
98
+ const rowData: any[] = [];
99
+ row.eachCell((cell, colNumber) => {
100
+ rowData.push({
101
+ value: cell.value,
102
+ type: cell.type,
103
+ formula: cell.formula
104
+ });
105
+ });
106
+ sheetData.data.push(rowData);
107
+ });
108
+
109
+ sheets.push(sheetData);
110
+ });
111
+
112
+ content = {
113
+ type: 'excel',
114
+ sheets,
115
+ sheetCount: sheets.length
116
+ };
117
+ } catch (error) {
118
+ content = {
119
+ type: 'excel',
120
+ error: 'Failed to process Excel spreadsheet',
121
+ details: error
122
+ };
123
+ }
124
+ break;
125
+
126
+ case 'pdf':
127
+ // For PDF, we'll return metadata for now
128
+ // Full PDF text extraction would require pdf-parse or similar
129
+ content = {
130
+ type: 'pdf',
131
+ fileName,
132
+ size: fileBuffer.length,
133
+ message: 'PDF processing requires additional libraries for text extraction'
134
+ };
135
+ break;
136
+
137
+ case 'pptx':
138
+ case 'ppt':
139
+ // PowerPoint processing would require additional libraries
140
+ content = {
141
+ type: 'powerpoint',
142
+ fileName,
143
+ size: fileBuffer.length,
144
+ message: 'PowerPoint processing requires additional libraries'
145
+ };
146
+ break;
147
+
148
+ case 'txt':
149
+ case 'md':
150
+ case 'json':
151
+ case 'csv':
152
+ // Text-based files
153
+ content = {
154
+ type: ext,
155
+ text: fileBuffer.toString('utf-8')
156
+ };
157
+ break;
158
+
159
+ default:
160
+ content = {
161
+ type: 'unknown',
162
+ fileName,
163
+ size: fileBuffer.length,
164
+ message: 'Unknown file type'
165
+ };
166
+ }
167
+
168
+ // Perform requested operation
169
+ if (operation === 'analyze' && content.text) {
170
+ // Basic text analysis
171
+ const text = content.text || '';
172
+ content.analysis = {
173
+ characterCount: text.length,
174
+ wordCount: text.split(/\s+/).filter(Boolean).length,
175
+ lineCount: text.split('\n').length,
176
+ paragraphCount: text.split('\n\n').filter(Boolean).length
177
+ };
178
+ }
179
+
180
+ return NextResponse.json({
181
+ success: true,
182
+ fileName,
183
+ operation,
184
+ content
185
+ });
186
+ } catch (error) {
187
+ console.error('Error processing document:', error);
188
+ return NextResponse.json(
189
+ { success: false, error: 'Failed to process document' },
190
+ { status: 500 }
191
+ );
192
+ }
193
+ }
194
+
195
+ export async function GET() {
196
+ return NextResponse.json({
197
+ message: 'Document processing endpoint',
198
+ endpoint: '/api/documents/process',
199
+ method: 'POST',
200
+ headers: {
201
+ 'x-session-key': 'Your session key (required)'
202
+ },
203
+ body: {
204
+ fileName: 'Name of the file to process',
205
+ isPublic: 'true/false - whether file is in public folder',
206
+ operation: 'Operation to perform: read (default), analyze'
207
+ },
208
+ supportedFormats: [
209
+ 'docx - Word documents (text extraction)',
210
+ 'xlsx/xls - Excel spreadsheets (data extraction)',
211
+ 'pdf - PDF files (metadata only)',
212
+ 'pptx/ppt - PowerPoint (metadata only)',
213
+ 'txt/md/json/csv - Text files (full content)'
214
+ ]
215
+ });
216
+ }
app/api/public/upload/route.ts ADDED
@@ -0,0 +1,58 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { NextRequest, NextResponse } from 'next/server';
2
+ import { SessionManager } from '@/lib/sessionManager';
3
+
4
+ export async function POST(request: NextRequest) {
5
+ try {
6
+ const sessionManager = SessionManager.getInstance();
7
+
8
+ // Get form data
9
+ const formData = await request.formData();
10
+ const file = formData.get('file') as File;
11
+
12
+ if (!file) {
13
+ return NextResponse.json(
14
+ { success: false, error: 'No file provided' },
15
+ { status: 400 }
16
+ );
17
+ }
18
+
19
+ // Read file content
20
+ const bytes = await file.arrayBuffer();
21
+ const buffer = Buffer.from(bytes);
22
+
23
+ // Save to public folder - NO AUTHENTICATION REQUIRED!
24
+ const filePath = await sessionManager.saveFileToPublic(file.name, buffer);
25
+
26
+ return NextResponse.json({
27
+ success: true,
28
+ message: 'File uploaded to public folder successfully!',
29
+ fileName: file.name,
30
+ size: file.size,
31
+ type: file.type,
32
+ isPublic: true,
33
+ path: filePath,
34
+ note: 'This file is publicly accessible to everyone'
35
+ });
36
+ } catch (error) {
37
+ console.error('Error uploading public file:', error);
38
+ return NextResponse.json(
39
+ { success: false, error: 'Failed to upload file' },
40
+ { status: 500 }
41
+ );
42
+ }
43
+ }
44
+
45
+ export async function GET() {
46
+ return NextResponse.json({
47
+ message: 'Public file upload endpoint - NO AUTHENTICATION REQUIRED',
48
+ endpoint: '/api/public/upload',
49
+ method: 'POST',
50
+ headers: {
51
+ 'x-session-key': 'NOT REQUIRED for public uploads'
52
+ },
53
+ body: {
54
+ file: 'File to upload (multipart/form-data)'
55
+ },
56
+ note: 'Files uploaded here are accessible to everyone. For private files, use /api/sessions/upload with a session key.'
57
+ });
58
+ }
app/api/sessions/create/route.ts ADDED
@@ -0,0 +1,40 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { NextRequest, NextResponse } from 'next/server';
2
+ import { SessionManager } from '@/lib/sessionManager';
3
+
4
+ export async function POST(request: NextRequest) {
5
+ try {
6
+ const sessionManager = SessionManager.getInstance();
7
+ const body = await request.json().catch(() => ({}));
8
+
9
+ const session = await sessionManager.createSession(body.metadata);
10
+
11
+ return NextResponse.json({
12
+ success: true,
13
+ session: {
14
+ id: session.id,
15
+ key: session.key,
16
+ createdAt: session.createdAt,
17
+ message: 'Session created successfully. Keep your session key secure!'
18
+ }
19
+ });
20
+ } catch (error) {
21
+ console.error('Error creating session:', error);
22
+ return NextResponse.json(
23
+ { success: false, error: 'Failed to create session' },
24
+ { status: 500 }
25
+ );
26
+ }
27
+ }
28
+
29
+ export async function GET() {
30
+ return NextResponse.json({
31
+ message: 'Use POST to create a new session',
32
+ endpoint: '/api/sessions/create',
33
+ method: 'POST',
34
+ body: {
35
+ metadata: {
36
+ description: 'Optional metadata object'
37
+ }
38
+ }
39
+ });
40
+ }
app/api/sessions/download/route.ts ADDED
@@ -0,0 +1,102 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { NextRequest, NextResponse } from 'next/server';
2
+ import { SessionManager } from '@/lib/sessionManager';
3
+
4
+ export async function GET(request: NextRequest) {
5
+ try {
6
+ const sessionManager = SessionManager.getInstance();
7
+ const { searchParams } = new URL(request.url);
8
+ const fileName = searchParams.get('file');
9
+ const isPublic = searchParams.get('public') === 'true';
10
+
11
+ if (!fileName) {
12
+ return NextResponse.json(
13
+ { success: false, error: 'File name is required' },
14
+ { status: 400 }
15
+ );
16
+ }
17
+
18
+ let fileBuffer: Buffer;
19
+
20
+ if (isPublic) {
21
+ // Get from public folder
22
+ try {
23
+ fileBuffer = await sessionManager.getFileFromPublic(fileName);
24
+ } catch (error) {
25
+ return NextResponse.json(
26
+ { success: false, error: 'File not found in public folder' },
27
+ { status: 404 }
28
+ );
29
+ }
30
+ } else {
31
+ // Get session key from headers
32
+ const sessionKey = request.headers.get('x-session-key');
33
+ if (!sessionKey) {
34
+ return NextResponse.json(
35
+ { success: false, error: 'Session key is required for private files' },
36
+ { status: 401 }
37
+ );
38
+ }
39
+
40
+ // Validate session
41
+ const isValid = await sessionManager.validateSession(sessionKey);
42
+ if (!isValid) {
43
+ return NextResponse.json(
44
+ { success: false, error: 'Invalid or expired session key' },
45
+ { status: 401 }
46
+ );
47
+ }
48
+
49
+ // Get from session folder
50
+ try {
51
+ fileBuffer = await sessionManager.getFileFromSession(sessionKey, fileName);
52
+ } catch (error) {
53
+ return NextResponse.json(
54
+ { success: false, error: 'File not found in session' },
55
+ { status: 404 }
56
+ );
57
+ }
58
+ }
59
+
60
+ // Determine content type based on file extension
61
+ const ext = fileName.split('.').pop()?.toLowerCase();
62
+ let contentType = 'application/octet-stream';
63
+
64
+ const contentTypes: Record<string, string> = {
65
+ 'pdf': 'application/pdf',
66
+ 'docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
67
+ 'doc': 'application/msword',
68
+ 'xlsx': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
69
+ 'xls': 'application/vnd.ms-excel',
70
+ 'pptx': 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
71
+ 'ppt': 'application/vnd.ms-powerpoint',
72
+ 'txt': 'text/plain',
73
+ 'json': 'application/json',
74
+ 'html': 'text/html',
75
+ 'css': 'text/css',
76
+ 'js': 'application/javascript',
77
+ 'png': 'image/png',
78
+ 'jpg': 'image/jpeg',
79
+ 'jpeg': 'image/jpeg',
80
+ 'gif': 'image/gif',
81
+ 'svg': 'image/svg+xml'
82
+ };
83
+
84
+ if (ext && contentTypes[ext]) {
85
+ contentType = contentTypes[ext];
86
+ }
87
+
88
+ return new NextResponse(fileBuffer as any, {
89
+ headers: {
90
+ 'Content-Type': contentType,
91
+ 'Content-Disposition': `attachment; filename="${fileName}"`,
92
+ 'Content-Length': fileBuffer.length.toString()
93
+ }
94
+ });
95
+ } catch (error) {
96
+ console.error('Error downloading file:', error);
97
+ return NextResponse.json(
98
+ { success: false, error: 'Failed to download file' },
99
+ { status: 500 }
100
+ );
101
+ }
102
+ }
app/api/sessions/files/route.ts ADDED
@@ -0,0 +1,102 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { NextRequest, NextResponse } from 'next/server';
2
+ import { SessionManager } from '@/lib/sessionManager';
3
+ import fs from 'fs/promises';
4
+ import path from 'path';
5
+
6
+ export async function GET(request: NextRequest) {
7
+ try {
8
+ const sessionManager = SessionManager.getInstance();
9
+ const { searchParams } = new URL(request.url);
10
+ const listPublic = searchParams.get('public') === 'true';
11
+
12
+ if (listPublic) {
13
+ // List public files
14
+ const publicPath = sessionManager.getPublicPath();
15
+ try {
16
+ const files = await fs.readdir(publicPath);
17
+ const fileDetails = await Promise.all(
18
+ files.map(async (fileName) => {
19
+ const filePath = path.join(publicPath, fileName);
20
+ const stats = await fs.stat(filePath);
21
+ return {
22
+ name: fileName,
23
+ size: stats.size,
24
+ modified: stats.mtime,
25
+ created: stats.ctime
26
+ };
27
+ })
28
+ );
29
+
30
+ return NextResponse.json({
31
+ success: true,
32
+ files: fileDetails,
33
+ count: fileDetails.length,
34
+ type: 'public'
35
+ });
36
+ } catch (error) {
37
+ return NextResponse.json({
38
+ success: true,
39
+ files: [],
40
+ count: 0,
41
+ type: 'public'
42
+ });
43
+ }
44
+ } else {
45
+ // List session files
46
+ const sessionKey = request.headers.get('x-session-key');
47
+ if (!sessionKey) {
48
+ return NextResponse.json(
49
+ { success: false, error: 'Session key is required for listing session files' },
50
+ { status: 401 }
51
+ );
52
+ }
53
+
54
+ // Validate session
55
+ const isValid = await sessionManager.validateSession(sessionKey);
56
+ if (!isValid) {
57
+ return NextResponse.json(
58
+ { success: false, error: 'Invalid or expired session key' },
59
+ { status: 401 }
60
+ );
61
+ }
62
+
63
+ const files = await sessionManager.listSessionFiles(sessionKey);
64
+ const sessionPath = await sessionManager.getSessionPath(sessionKey);
65
+
66
+ if (!sessionPath) {
67
+ return NextResponse.json({
68
+ success: true,
69
+ files: [],
70
+ count: 0,
71
+ type: 'session'
72
+ });
73
+ }
74
+
75
+ const fileDetails = await Promise.all(
76
+ files.map(async (fileName) => {
77
+ const filePath = path.join(sessionPath, fileName);
78
+ const stats = await fs.stat(filePath);
79
+ return {
80
+ name: fileName,
81
+ size: stats.size,
82
+ modified: stats.mtime,
83
+ created: stats.ctime
84
+ };
85
+ })
86
+ );
87
+
88
+ return NextResponse.json({
89
+ success: true,
90
+ files: fileDetails,
91
+ count: fileDetails.length,
92
+ type: 'session'
93
+ });
94
+ }
95
+ } catch (error) {
96
+ console.error('Error listing files:', error);
97
+ return NextResponse.json(
98
+ { success: false, error: 'Failed to list files' },
99
+ { status: 500 }
100
+ );
101
+ }
102
+ }
app/api/sessions/upload/route.ts ADDED
@@ -0,0 +1,87 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { NextRequest, NextResponse } from 'next/server';
2
+ import { SessionManager } from '@/lib/sessionManager';
3
+
4
+ export async function POST(request: NextRequest) {
5
+ try {
6
+ const sessionManager = SessionManager.getInstance();
7
+
8
+ // Get form data
9
+ const formData = await request.formData();
10
+ const file = formData.get('file') as File;
11
+ const isPublic = formData.get('public') === 'true';
12
+
13
+ // Get session key from headers
14
+ const sessionKey = request.headers.get('x-session-key');
15
+
16
+ // If uploading to session folder, require session key
17
+ if (!isPublic) {
18
+ if (!sessionKey) {
19
+ return NextResponse.json(
20
+ { success: false, error: 'Session key is required for private uploads. Use public=true for public uploads.' },
21
+ { status: 401 }
22
+ );
23
+ }
24
+
25
+ // Validate session
26
+ const isValid = await sessionManager.validateSession(sessionKey);
27
+ if (!isValid) {
28
+ return NextResponse.json(
29
+ { success: false, error: 'Invalid or expired session key' },
30
+ { status: 401 }
31
+ );
32
+ }
33
+ }
34
+ // Public uploads don't need authentication!
35
+
36
+ if (!file) {
37
+ return NextResponse.json(
38
+ { success: false, error: 'No file provided' },
39
+ { status: 400 }
40
+ );
41
+ }
42
+
43
+ // Read file content
44
+ const bytes = await file.arrayBuffer();
45
+ const buffer = Buffer.from(bytes);
46
+
47
+ let filePath: string;
48
+ if (isPublic) {
49
+ // Save to public folder - no authentication needed
50
+ filePath = await sessionManager.saveFileToPublic(file.name, buffer);
51
+ } else {
52
+ // Save to session folder - requires valid session key
53
+ filePath = await sessionManager.saveFileToSession(sessionKey!, file.name, buffer);
54
+ }
55
+
56
+ return NextResponse.json({
57
+ success: true,
58
+ message: 'File uploaded successfully',
59
+ fileName: file.name,
60
+ size: file.size,
61
+ type: file.type,
62
+ isPublic,
63
+ path: filePath
64
+ });
65
+ } catch (error) {
66
+ console.error('Error uploading file:', error);
67
+ return NextResponse.json(
68
+ { success: false, error: 'Failed to upload file' },
69
+ { status: 500 }
70
+ );
71
+ }
72
+ }
73
+
74
+ export async function GET() {
75
+ return NextResponse.json({
76
+ message: 'File upload endpoint',
77
+ endpoint: '/api/sessions/upload',
78
+ method: 'POST',
79
+ headers: {
80
+ 'x-session-key': 'Your session key (required)'
81
+ },
82
+ body: {
83
+ file: 'File to upload (multipart/form-data)',
84
+ public: 'true/false - whether to save in public folder (optional, default: false)'
85
+ }
86
+ });
87
+ }
app/components/CodeExecutor.tsx DELETED
@@ -1,306 +0,0 @@
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 DELETED
@@ -1,668 +0,0 @@
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/Desktop.tsx CHANGED
@@ -16,11 +16,9 @@ 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)
@@ -29,12 +27,12 @@ export function Desktop() {
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)
@@ -43,7 +41,7 @@ export function Desktop() {
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)
@@ -94,28 +92,12 @@ export function Desktop() {
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) => {
@@ -138,14 +120,8 @@ export function Desktop() {
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
  }
@@ -206,6 +182,55 @@ export function Desktop() {
206
  }
207
  }
208
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
209
  // Keyboard shortcuts
210
  useEffect(() => {
211
  const handleKeyDown = (e: KeyboardEvent) => {
@@ -273,7 +298,7 @@ export function Desktop() {
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 */}
@@ -335,28 +360,12 @@ export function Desktop() {
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
 
@@ -418,35 +427,23 @@ export function Desktop() {
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 */}
 
16
  import { Terminal } from './Terminal'
17
  import { SpotlightSearch } from './SpotlightSearch'
18
  import { ContextMenu } from './ContextMenu'
 
 
19
  import { AboutModal } from './AboutModal'
20
+ import { SessionManagerWindow } from './SessionManagerWindow'
21
+ import { motion, AnimatePresence } from 'framer-motion'
22
 
23
  export function Desktop() {
24
  const [fileManagerOpen, setFileManagerOpen] = useState(true)
 
27
  const [browserOpen, setBrowserOpen] = useState(false)
28
  const [geminiChatOpen, setGeminiChatOpen] = useState(false)
29
  const [terminalOpen, setTerminalOpen] = useState(false)
 
 
30
  const [spotlightOpen, setSpotlightOpen] = useState(false)
31
  const [contextMenuVisible, setContextMenuVisible] = useState(false)
32
  const [contextMenuPosition, setContextMenuPosition] = useState({ x: 0, y: 0 })
33
+ const [userSession, setUserSession] = useState<string>('')
34
+ const [sessionKey, setSessionKey] = useState<string>('')
35
+ const [sessionInitialized, setSessionInitialized] = useState(false)
36
  const [currentPath, setCurrentPath] = useState('')
37
  const [matrixActive, setMatrixActive] = useState(false)
38
  const [helpModalOpen, setHelpModalOpen] = useState(false)
 
41
  const [backgroundSelectorOpen, setBackgroundSelectorOpen] = useState(false)
42
  const [currentBackground, setCurrentBackground] = useState('/background.webp')
43
  const [aboutModalOpen, setAboutModalOpen] = useState(false)
44
+ const [sessionManagerOpen, setSessionManagerOpen] = useState(false)
45
 
46
  const openFileManager = (path: string) => {
47
  setCurrentPath(path)
 
92
  setTerminalOpen(false)
93
  }
94
 
95
+ const openSessionManager = () => {
96
+ setSessionManagerOpen(true)
97
  }
98
 
99
+ const closeSessionManager = () => {
100
+ setSessionManagerOpen(false)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
101
  }
102
 
103
  const handleOpenApp = (appId: string) => {
 
120
  case 'terminal':
121
  openTerminal()
122
  break
123
+ case 'sessions':
124
+ openSessionManager()
 
 
 
 
 
 
125
  break
126
  }
127
  }
 
182
  }
183
  }
184
 
185
+ // Initialize session automatically on mount
186
+ useEffect(() => {
187
+ const initializeSession = async () => {
188
+ // Check if session already exists in localStorage
189
+ const savedSessionId = localStorage.getItem('reubenOS_sessionId')
190
+ const savedSessionKey = localStorage.getItem('reubenOS_sessionKey')
191
+
192
+ if (savedSessionId && savedSessionKey) {
193
+ // Use existing session
194
+ setUserSession(savedSessionId)
195
+ setSessionKey(savedSessionKey)
196
+ setSessionInitialized(true)
197
+ console.log('✅ Loaded existing session:', savedSessionId)
198
+ } else {
199
+ // Create new session automatically
200
+ try {
201
+ const response = await fetch('/api/sessions/create', {
202
+ method: 'POST',
203
+ headers: { 'Content-Type': 'application/json' },
204
+ body: JSON.stringify({
205
+ metadata: {
206
+ createdAt: new Date().toISOString(),
207
+ autoCreated: true
208
+ }
209
+ })
210
+ })
211
+
212
+ const data = await response.json()
213
+
214
+ if (data.success) {
215
+ setUserSession(data.session.id)
216
+ setSessionKey(data.session.key)
217
+ setSessionInitialized(true)
218
+
219
+ // Save to localStorage
220
+ localStorage.setItem('reubenOS_sessionId', data.session.id)
221
+ localStorage.setItem('reubenOS_sessionKey', data.session.key)
222
+
223
+ console.log('✅ Auto-created new session:', data.session.id)
224
+ }
225
+ } catch (error) {
226
+ console.error('Failed to create session:', error)
227
+ }
228
+ }
229
+ }
230
+
231
+ initializeSession()
232
+ }, [])
233
+
234
  // Keyboard shortcuts
235
  useEffect(() => {
236
  const handleKeyDown = (e: KeyboardEvent) => {
 
298
 
299
  <TopBar onPowerAction={triggerMatrix} onAboutClick={() => setAboutModalOpen(true)} />
300
 
301
+ <Dock onOpenFileManager={openFileManager} onOpenCalendar={openCalendar} onOpenClock={openClock} onOpenBrowser={openBrowser} onOpenGeminiChat={openGeminiChat} />
302
 
303
  <div className="flex-1">
304
  {/* Desktop Icons - Positioned in a grid layout */}
 
360
  onDoubleClick={() => openFileManager('')}
361
  />
362
  <DraggableDesktopIcon
363
+ id="sessions"
364
+ label="Sessions"
365
+ iconType="key"
366
+ initialPosition={{ x: 220, y: 120 }}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
367
  onClick={() => {}}
368
+ onDoubleClick={openSessionManager}
369
  />
370
  </div>
371
 
 
427
  </motion.div>
428
  )}
429
 
430
+ <AnimatePresence>
431
+ {sessionManagerOpen && sessionInitialized && (
432
+ <motion.div
433
+ initial={{ scale: 0.9, opacity: 0 }}
434
+ animate={{ scale: 1, opacity: 1 }}
435
+ exit={{ scale: 0.9, opacity: 0 }}
436
+ transition={{ duration: 0.2, ease: "easeInOut" }}
437
+ >
438
+ <SessionManagerWindow
439
+ onClose={closeSessionManager}
440
+ sessionId={userSession}
441
+ sessionKey={sessionKey}
442
+ />
443
+ </motion.div>
444
+ )}
445
+ </AnimatePresence>
446
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
447
  </div>
448
 
449
  {/* Spotlight Search */}
app/components/Dock.tsx CHANGED
@@ -9,9 +9,7 @@ import {
9
  Sparkle,
10
  Trash,
11
  FolderOpen,
12
- Compass,
13
- Code,
14
- Lightning
15
  } from '@phosphor-icons/react'
16
 
17
  interface DockProps {
@@ -20,8 +18,6 @@ interface DockProps {
20
  onOpenClock: () => void
21
  onOpenBrowser: () => void
22
  onOpenGeminiChat: () => void
23
- onOpenVSCode?: () => void
24
- onOpenCodePlayground?: () => void
25
  }
26
 
27
  interface DockItemProps {
@@ -59,9 +55,7 @@ export function Dock({
59
  onOpenCalendar,
60
  onOpenClock,
61
  onOpenBrowser,
62
- onOpenGeminiChat,
63
- onOpenVSCode,
64
- onOpenCodePlayground
65
  }: DockProps) {
66
  const dockItems = [
67
  {
@@ -118,26 +112,6 @@ export function Dock({
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
 
 
9
  Sparkle,
10
  Trash,
11
  FolderOpen,
12
+ Compass
 
 
13
  } from '@phosphor-icons/react'
14
 
15
  interface DockProps {
 
18
  onOpenClock: () => void
19
  onOpenBrowser: () => void
20
  onOpenGeminiChat: () => void
 
 
21
  }
22
 
23
  interface DockItemProps {
 
55
  onOpenCalendar,
56
  onOpenClock,
57
  onOpenBrowser,
58
+ onOpenGeminiChat
 
 
59
  }: DockProps) {
60
  const dockItems = [
61
  {
 
112
  label: 'Calendar',
113
  onClick: onOpenCalendar,
114
  className: ''
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
115
  }
116
  ]
117
 
app/components/DraggableDesktopIcon.tsx CHANGED
@@ -12,7 +12,8 @@ import {
12
  HardDrives,
13
  Compass,
14
  Code,
15
- Lightning
 
16
  } from '@phosphor-icons/react'
17
 
18
  interface DraggableDesktopIconProps {
@@ -100,6 +101,12 @@ export function DraggableDesktopIcon({
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">
 
12
  HardDrives,
13
  Compass,
14
  Code,
15
+ Lightning,
16
+ Key
17
  } from '@phosphor-icons/react'
18
 
19
  interface DraggableDesktopIconProps {
 
101
  <Lightning size={32} weight="fill" className="text-white" />
102
  </div>
103
  )
104
+ case 'key':
105
+ return (
106
+ <div className="bg-gradient-to-br from-purple-600 to-purple-800 w-full h-full rounded-xl flex items-center justify-center border border-purple-900/30 shadow-inner">
107
+ <Key size={32} weight="bold" className="text-white" />
108
+ </div>
109
+ )
110
  default:
111
  return (
112
  <div className="bg-gray-400 w-full h-full rounded-xl flex items-center justify-center">
app/components/SessionManager.tsx ADDED
@@ -0,0 +1,543 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ 'use client'
2
+
3
+ import React, { useState, useEffect } from 'react'
4
+ import { motion, AnimatePresence } from 'framer-motion'
5
+ import {
6
+ Key,
7
+ Upload,
8
+ Download,
9
+ File,
10
+ Folder,
11
+ Globe,
12
+ Lock,
13
+ Copy,
14
+ Check,
15
+ X,
16
+ Trash,
17
+ FileText,
18
+ Table as FileSpreadsheet,
19
+ Presentation as FilePresentation,
20
+ FilePdf,
21
+ Plus,
22
+ ArrowsClockwise as RefreshCw
23
+ } from '@phosphor-icons/react'
24
+
25
+ interface Session {
26
+ id: string
27
+ key: string
28
+ createdAt: string
29
+ message?: string
30
+ }
31
+
32
+ interface FileItem {
33
+ name: string
34
+ size: number
35
+ modified: string
36
+ created: string
37
+ }
38
+
39
+ export function SessionManager() {
40
+ const [currentSession, setCurrentSession] = useState<Session | null>(null)
41
+ const [sessionKey, setSessionKey] = useState('')
42
+ const [files, setFiles] = useState<FileItem[]>([])
43
+ const [publicFiles, setPublicFiles] = useState<FileItem[]>([])
44
+ const [loading, setLoading] = useState(false)
45
+ const [error, setError] = useState<string | null>(null)
46
+ const [success, setSuccess] = useState<string | null>(null)
47
+ const [copiedKey, setCopiedKey] = useState(false)
48
+ const [activeTab, setActiveTab] = useState<'session' | 'public'>('session')
49
+ const [uploadFile, setUploadFile] = useState<File | null>(null)
50
+ const [isPublicUpload, setIsPublicUpload] = useState(false)
51
+
52
+ // Create new session
53
+ const createNewSession = async () => {
54
+ setLoading(true)
55
+ setError(null)
56
+ try {
57
+ const response = await fetch('/api/sessions/create', {
58
+ method: 'POST',
59
+ headers: { 'Content-Type': 'application/json' },
60
+ body: JSON.stringify({ metadata: { createdFrom: 'UI' } })
61
+ })
62
+ const data = await response.json()
63
+
64
+ if (data.success) {
65
+ setCurrentSession(data.session)
66
+ setSessionKey(data.session.key)
67
+ setSuccess('Session created successfully!')
68
+ localStorage.setItem('reubenOS_sessionKey', data.session.key)
69
+ await loadSessionFiles(data.session.key)
70
+ } else {
71
+ setError(data.error || 'Failed to create session')
72
+ }
73
+ } catch (err) {
74
+ setError('Failed to create session')
75
+ }
76
+ setLoading(false)
77
+ }
78
+
79
+ // Load files for current session
80
+ const loadSessionFiles = async (key: string) => {
81
+ try {
82
+ const response = await fetch('/api/sessions/files', {
83
+ headers: { 'x-session-key': key }
84
+ })
85
+ const data = await response.json()
86
+
87
+ if (data.success) {
88
+ setFiles(data.files || [])
89
+ }
90
+ } catch (err) {
91
+ console.error('Failed to load session files:', err)
92
+ }
93
+ }
94
+
95
+ // Load public files
96
+ const loadPublicFiles = async () => {
97
+ try {
98
+ const response = await fetch('/api/sessions/files?public=true')
99
+ const data = await response.json()
100
+
101
+ if (data.success) {
102
+ setPublicFiles(data.files || [])
103
+ }
104
+ } catch (err) {
105
+ console.error('Failed to load public files:', err)
106
+ }
107
+ }
108
+
109
+ // Handle file upload
110
+ const handleFileUpload = async () => {
111
+ if (!uploadFile || !sessionKey) {
112
+ setError('No file selected or session not active')
113
+ return
114
+ }
115
+
116
+ setLoading(true)
117
+ setError(null)
118
+ const formData = new FormData()
119
+ formData.append('file', uploadFile)
120
+ formData.append('public', isPublicUpload.toString())
121
+
122
+ try {
123
+ const response = await fetch('/api/sessions/upload', {
124
+ method: 'POST',
125
+ headers: { 'x-session-key': sessionKey },
126
+ body: formData
127
+ })
128
+ const data = await response.json()
129
+
130
+ if (data.success) {
131
+ setSuccess(`File uploaded successfully: ${data.fileName}`)
132
+ setUploadFile(null)
133
+ if (isPublicUpload) {
134
+ await loadPublicFiles()
135
+ } else {
136
+ await loadSessionFiles(sessionKey)
137
+ }
138
+ } else {
139
+ setError(data.error || 'Failed to upload file')
140
+ }
141
+ } catch (err) {
142
+ setError('Failed to upload file')
143
+ }
144
+ setLoading(false)
145
+ }
146
+
147
+ // Download file
148
+ const downloadFile = async (fileName: string, isPublic: boolean) => {
149
+ const url = `/api/sessions/download?file=${encodeURIComponent(fileName)}${isPublic ? '&public=true' : ''}`
150
+ const headers: HeadersInit = isPublic ? {} : { 'x-session-key': sessionKey }
151
+
152
+ try {
153
+ const response = await fetch(url, { headers })
154
+ if (response.ok) {
155
+ const blob = await response.blob()
156
+ const downloadUrl = window.URL.createObjectURL(blob)
157
+ const a = document.createElement('a')
158
+ a.href = downloadUrl
159
+ a.download = fileName
160
+ document.body.appendChild(a)
161
+ a.click()
162
+ window.URL.revokeObjectURL(downloadUrl)
163
+ document.body.removeChild(a)
164
+ setSuccess(`Downloaded: ${fileName}`)
165
+ } else {
166
+ setError('Failed to download file')
167
+ }
168
+ } catch (err) {
169
+ setError('Failed to download file')
170
+ }
171
+ }
172
+
173
+ // Generate document
174
+ const generateSampleDocument = async (type: string) => {
175
+ if (!sessionKey) {
176
+ setError('No active session')
177
+ return
178
+ }
179
+
180
+ setLoading(true)
181
+ setError(null)
182
+
183
+ const sampleContent: any = {
184
+ docx: {
185
+ type: 'docx',
186
+ fileName: 'sample-document',
187
+ content: {
188
+ title: 'Sample Document',
189
+ content: '# Introduction\n\nThis is a sample document generated by ReubenOS.\n\n## Features\n\n- Session-based isolation\n- Document generation\n- File management\n\n## Conclusion\n\nThank you for using ReubenOS!'
190
+ }
191
+ },
192
+ pdf: {
193
+ type: 'pdf',
194
+ fileName: 'sample-report',
195
+ content: {
196
+ title: 'Sample PDF Report',
197
+ content: 'This is a sample PDF report.\n\n# Section 1\n\nLorem ipsum dolor sit amet.\n\n# Section 2\n\nConclusion and summary.'
198
+ }
199
+ },
200
+ ppt: {
201
+ type: 'ppt',
202
+ fileName: 'sample-presentation',
203
+ content: {
204
+ slides: [
205
+ { title: 'Welcome', content: 'Welcome to ReubenOS', bullets: ['Feature 1', 'Feature 2'] },
206
+ { title: 'Overview', content: 'System Overview', bullets: ['Session Management', 'Document Generation'] },
207
+ { title: 'Thank You', content: 'Questions?' }
208
+ ]
209
+ }
210
+ },
211
+ excel: {
212
+ type: 'excel',
213
+ fileName: 'sample-data',
214
+ content: {
215
+ sheets: [{
216
+ name: 'Sample Data',
217
+ data: {
218
+ headers: ['Name', 'Value', 'Status'],
219
+ rows: [
220
+ ['Item 1', '100', 'Active'],
221
+ ['Item 2', '200', 'Pending'],
222
+ ['Item 3', '150', 'Active']
223
+ ]
224
+ }
225
+ }]
226
+ }
227
+ }
228
+ }
229
+
230
+ const documentData = sampleContent[type]
231
+ if (!documentData) {
232
+ setError('Invalid document type')
233
+ setLoading(false)
234
+ return
235
+ }
236
+
237
+ try {
238
+ const response = await fetch('/api/documents/generate', {
239
+ method: 'POST',
240
+ headers: {
241
+ 'Content-Type': 'application/json',
242
+ 'x-session-key': sessionKey
243
+ },
244
+ body: JSON.stringify({ ...documentData, isPublic: false })
245
+ })
246
+ const data = await response.json()
247
+
248
+ if (data.success) {
249
+ setSuccess(`Generated ${type.toUpperCase()}: ${data.fileName}`)
250
+ await loadSessionFiles(sessionKey)
251
+ } else {
252
+ setError(data.error || `Failed to generate ${type}`)
253
+ }
254
+ } catch (err) {
255
+ setError(`Failed to generate ${type}`)
256
+ }
257
+ setLoading(false)
258
+ }
259
+
260
+ // Copy session key
261
+ const copySessionKey = () => {
262
+ navigator.clipboard.writeText(sessionKey)
263
+ setCopiedKey(true)
264
+ setTimeout(() => setCopiedKey(false), 2000)
265
+ }
266
+
267
+ // Get file icon based on extension
268
+ const getFileIcon = (fileName: string) => {
269
+ const ext = fileName.split('.').pop()?.toLowerCase()
270
+ switch (ext) {
271
+ case 'docx':
272
+ case 'doc':
273
+ return <FileText size={20} weight="fill" className="text-blue-500" />
274
+ case 'xlsx':
275
+ case 'xls':
276
+ return <FileSpreadsheet size={20} weight="fill" className="text-green-500" />
277
+ case 'pptx':
278
+ case 'ppt':
279
+ return <FilePresentation size={20} weight="fill" className="text-orange-500" />
280
+ case 'pdf':
281
+ return <FilePdf size={20} weight="fill" className="text-red-500" />
282
+ default:
283
+ return <File size={20} weight="fill" className="text-gray-500" />
284
+ }
285
+ }
286
+
287
+ // Load saved session on mount
288
+ useEffect(() => {
289
+ const savedKey = localStorage.getItem('reubenOS_sessionKey')
290
+ if (savedKey) {
291
+ setSessionKey(savedKey)
292
+ loadSessionFiles(savedKey)
293
+ }
294
+ loadPublicFiles()
295
+ }, [])
296
+
297
+ // Clear messages after 3 seconds
298
+ useEffect(() => {
299
+ if (success) {
300
+ const timer = setTimeout(() => setSuccess(null), 3000)
301
+ return () => clearTimeout(timer)
302
+ }
303
+ }, [success])
304
+
305
+ useEffect(() => {
306
+ if (error) {
307
+ const timer = setTimeout(() => setError(null), 3000)
308
+ return () => clearTimeout(timer)
309
+ }
310
+ }, [error])
311
+
312
+ return (
313
+ <div className="min-h-screen bg-gradient-to-br from-purple-900/20 via-black to-purple-900/20 p-8">
314
+ <div className="max-w-6xl mx-auto">
315
+ <h1 className="text-4xl font-bold text-white mb-8">ReubenOS Session Manager</h1>
316
+
317
+ {/* Session Info */}
318
+ <div className="bg-gray-900/50 backdrop-blur-lg rounded-xl p-6 mb-8 border border-purple-500/20">
319
+ {currentSession ? (
320
+ <div>
321
+ <div className="flex items-center justify-between mb-4">
322
+ <h2 className="text-xl font-semibold text-white">Active Session</h2>
323
+ <button
324
+ onClick={createNewSession}
325
+ className="px-4 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700 transition-colors"
326
+ >
327
+ <Plus size={20} className="inline mr-2" />
328
+ New Session
329
+ </button>
330
+ </div>
331
+ <div className="space-y-2">
332
+ <div className="flex items-center gap-2">
333
+ <Key size={20} className="text-purple-400" />
334
+ <span className="text-gray-400">Session Key:</span>
335
+ <code className="text-xs text-purple-300 bg-black/30 px-2 py-1 rounded">
336
+ {sessionKey.substring(0, 20)}...
337
+ </code>
338
+ <button
339
+ onClick={copySessionKey}
340
+ className="p-1 hover:bg-white/10 rounded transition-colors"
341
+ >
342
+ {copiedKey ? (
343
+ <Check size={16} className="text-green-400" />
344
+ ) : (
345
+ <Copy size={16} className="text-gray-400" />
346
+ )}
347
+ </button>
348
+ </div>
349
+ <div className="flex items-center gap-2">
350
+ <span className="text-gray-400">Created:</span>
351
+ <span className="text-white">{new Date(currentSession.createdAt).toLocaleString()}</span>
352
+ </div>
353
+ </div>
354
+ </div>
355
+ ) : (
356
+ <div className="text-center py-8">
357
+ <Key size={48} className="text-gray-600 mx-auto mb-4" />
358
+ <p className="text-gray-400 mb-4">No active session</p>
359
+ <button
360
+ onClick={createNewSession}
361
+ disabled={loading}
362
+ className="px-6 py-3 bg-purple-600 text-white rounded-lg hover:bg-purple-700 transition-colors disabled:opacity-50"
363
+ >
364
+ {loading ? 'Creating...' : 'Create New Session'}
365
+ </button>
366
+ </div>
367
+ )}
368
+ </div>
369
+
370
+ {/* File Upload */}
371
+ {currentSession && (
372
+ <div className="bg-gray-900/50 backdrop-blur-lg rounded-xl p-6 mb-8 border border-purple-500/20">
373
+ <h3 className="text-lg font-semibold text-white mb-4">Upload File</h3>
374
+ <div className="flex gap-4">
375
+ <input
376
+ type="file"
377
+ onChange={(e) => setUploadFile(e.target.files?.[0] || null)}
378
+ className="flex-1 text-white file:mr-4 file:py-2 file:px-4 file:rounded-lg file:border-0 file:bg-purple-600 file:text-white hover:file:bg-purple-700"
379
+ />
380
+ <label className="flex items-center gap-2 text-white">
381
+ <input
382
+ type="checkbox"
383
+ checked={isPublicUpload}
384
+ onChange={(e) => setIsPublicUpload(e.target.checked)}
385
+ className="rounded"
386
+ />
387
+ Public
388
+ </label>
389
+ <button
390
+ onClick={handleFileUpload}
391
+ disabled={!uploadFile || loading}
392
+ className="px-4 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700 transition-colors disabled:opacity-50"
393
+ >
394
+ <Upload size={20} className="inline mr-2" />
395
+ Upload
396
+ </button>
397
+ </div>
398
+ </div>
399
+ )}
400
+
401
+ {/* Document Generation */}
402
+ {currentSession && (
403
+ <div className="bg-gray-900/50 backdrop-blur-lg rounded-xl p-6 mb-8 border border-purple-500/20">
404
+ <h3 className="text-lg font-semibold text-white mb-4">Generate Documents</h3>
405
+ <div className="grid grid-cols-4 gap-4">
406
+ <button
407
+ onClick={() => generateSampleDocument('docx')}
408
+ disabled={loading}
409
+ className="p-4 bg-blue-600/20 border border-blue-500/30 rounded-lg hover:bg-blue-600/30 transition-colors disabled:opacity-50"
410
+ >
411
+ <FileText size={32} className="text-blue-400 mx-auto mb-2" />
412
+ <span className="text-white text-sm">Word Doc</span>
413
+ </button>
414
+ <button
415
+ onClick={() => generateSampleDocument('pdf')}
416
+ disabled={loading}
417
+ className="p-4 bg-red-600/20 border border-red-500/30 rounded-lg hover:bg-red-600/30 transition-colors disabled:opacity-50"
418
+ >
419
+ <FilePdf size={32} className="text-red-400 mx-auto mb-2" />
420
+ <span className="text-white text-sm">PDF</span>
421
+ </button>
422
+ <button
423
+ onClick={() => generateSampleDocument('ppt')}
424
+ disabled={loading}
425
+ className="p-4 bg-orange-600/20 border border-orange-500/30 rounded-lg hover:bg-orange-600/30 transition-colors disabled:opacity-50"
426
+ >
427
+ <FilePresentation size={32} className="text-orange-400 mx-auto mb-2" />
428
+ <span className="text-white text-sm">PowerPoint</span>
429
+ </button>
430
+ <button
431
+ onClick={() => generateSampleDocument('excel')}
432
+ disabled={loading}
433
+ className="p-4 bg-green-600/20 border border-green-500/30 rounded-lg hover:bg-green-600/30 transition-colors disabled:opacity-50"
434
+ >
435
+ <FileSpreadsheet size={32} className="text-green-400 mx-auto mb-2" />
436
+ <span className="text-white text-sm">Excel</span>
437
+ </button>
438
+ </div>
439
+ </div>
440
+ )}
441
+
442
+ {/* Files List */}
443
+ <div className="bg-gray-900/50 backdrop-blur-lg rounded-xl p-6 border border-purple-500/20">
444
+ {/* Tabs */}
445
+ <div className="flex gap-4 mb-6">
446
+ <button
447
+ onClick={() => setActiveTab('session')}
448
+ className={`px-4 py-2 rounded-lg transition-colors flex items-center gap-2 ${
449
+ activeTab === 'session'
450
+ ? 'bg-purple-600 text-white'
451
+ : 'bg-gray-800 text-gray-400 hover:bg-gray-700'
452
+ }`}
453
+ >
454
+ <Lock size={20} />
455
+ Session Files ({files.length})
456
+ </button>
457
+ <button
458
+ onClick={() => setActiveTab('public')}
459
+ className={`px-4 py-2 rounded-lg transition-colors flex items-center gap-2 ${
460
+ activeTab === 'public'
461
+ ? 'bg-purple-600 text-white'
462
+ : 'bg-gray-800 text-gray-400 hover:bg-gray-700'
463
+ }`}
464
+ >
465
+ <Globe size={20} />
466
+ Public Files ({publicFiles.length})
467
+ </button>
468
+ <button
469
+ onClick={() => {
470
+ if (activeTab === 'session' && sessionKey) {
471
+ loadSessionFiles(sessionKey)
472
+ } else {
473
+ loadPublicFiles()
474
+ }
475
+ }}
476
+ className="ml-auto p-2 bg-gray-800 text-gray-400 rounded-lg hover:bg-gray-700 transition-colors"
477
+ >
478
+ <RefreshCw size={20} />
479
+ </button>
480
+ </div>
481
+
482
+ {/* File List */}
483
+ <div className="space-y-2">
484
+ {(activeTab === 'session' ? files : publicFiles).map((file) => (
485
+ <div
486
+ key={file.name}
487
+ className="flex items-center justify-between p-3 bg-gray-800/50 rounded-lg hover:bg-gray-800/70 transition-colors"
488
+ >
489
+ <div className="flex items-center gap-3">
490
+ {getFileIcon(file.name)}
491
+ <div>
492
+ <p className="text-white font-medium">{file.name}</p>
493
+ <p className="text-gray-400 text-sm">
494
+ {(file.size / 1024).toFixed(2)} KB • Modified: {new Date(file.modified).toLocaleDateString()}
495
+ </p>
496
+ </div>
497
+ </div>
498
+ <button
499
+ onClick={() => downloadFile(file.name, activeTab === 'public')}
500
+ className="p-2 bg-purple-600/20 text-purple-400 rounded-lg hover:bg-purple-600/30 transition-colors"
501
+ >
502
+ <Download size={20} />
503
+ </button>
504
+ </div>
505
+ ))}
506
+ {(activeTab === 'session' ? files : publicFiles).length === 0 && (
507
+ <div className="text-center py-8">
508
+ <Folder size={48} className="text-gray-600 mx-auto mb-4" />
509
+ <p className="text-gray-400">No files in {activeTab === 'session' ? 'session' : 'public'} folder</p>
510
+ </div>
511
+ )}
512
+ </div>
513
+ </div>
514
+
515
+ {/* Messages */}
516
+ <AnimatePresence>
517
+ {success && (
518
+ <motion.div
519
+ initial={{ opacity: 0, y: 50 }}
520
+ animate={{ opacity: 1, y: 0 }}
521
+ exit={{ opacity: 0, y: 50 }}
522
+ className="fixed bottom-8 right-8 px-6 py-3 bg-green-600 text-white rounded-lg shadow-lg"
523
+ >
524
+ <Check size={20} className="inline mr-2" />
525
+ {success}
526
+ </motion.div>
527
+ )}
528
+ {error && (
529
+ <motion.div
530
+ initial={{ opacity: 0, y: 50 }}
531
+ animate={{ opacity: 1, y: 0 }}
532
+ exit={{ opacity: 0, y: 50 }}
533
+ className="fixed bottom-8 right-8 px-6 py-3 bg-red-600 text-white rounded-lg shadow-lg"
534
+ >
535
+ <X size={20} className="inline mr-2" />
536
+ {error}
537
+ </motion.div>
538
+ )}
539
+ </AnimatePresence>
540
+ </div>
541
+ </div>
542
+ )
543
+ }
app/components/SessionManagerWindow.tsx ADDED
@@ -0,0 +1,602 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ 'use client'
2
+
3
+ import React, { useState, useEffect, useRef } from 'react'
4
+ import Draggable from 'react-draggable'
5
+ import { motion, AnimatePresence } from 'framer-motion'
6
+ import {
7
+ Key,
8
+ Upload,
9
+ Download,
10
+ File,
11
+ Folder,
12
+ Globe,
13
+ Lock,
14
+ Copy,
15
+ Check,
16
+ X,
17
+ Trash,
18
+ FileText,
19
+ Table as FileSpreadsheet,
20
+ Presentation as FilePresentation,
21
+ FilePdf,
22
+ Plus,
23
+ ArrowsClockwise as RefreshCw,
24
+ Minus,
25
+ Square,
26
+ XCircle
27
+ } from '@phosphor-icons/react'
28
+
29
+ interface Session {
30
+ id: string
31
+ key: string
32
+ createdAt: string
33
+ message?: string
34
+ }
35
+
36
+ interface FileItem {
37
+ name: string
38
+ size: number
39
+ modified: string
40
+ created: string
41
+ }
42
+
43
+ interface SessionManagerWindowProps {
44
+ onClose: () => void
45
+ sessionId: string
46
+ sessionKey: string
47
+ }
48
+
49
+ export function SessionManagerWindow({ onClose, sessionId, sessionKey: initialSessionKey }: SessionManagerWindowProps) {
50
+ const [isMaximized, setIsMaximized] = useState(false)
51
+ const [sessionKey] = useState(initialSessionKey)
52
+ const [files, setFiles] = useState<FileItem[]>([])
53
+ const [publicFiles, setPublicFiles] = useState<FileItem[]>([])
54
+ const [loading, setLoading] = useState(false)
55
+ const [error, setError] = useState<string | null>(null)
56
+ const [success, setSuccess] = useState<string | null>(null)
57
+ const [copiedKey, setCopiedKey] = useState(false)
58
+ const [copiedId, setCopiedId] = useState(false)
59
+ const [activeTab, setActiveTab] = useState<'session' | 'public'>('session')
60
+ const [uploadFile, setUploadFile] = useState<File | null>(null)
61
+ const [isPublicUpload, setIsPublicUpload] = useState(false)
62
+ const nodeRef = useRef(null)
63
+
64
+ // Session is automatically created - no manual creation needed!
65
+
66
+ // Load files for current session
67
+ const loadSessionFiles = async (key: string) => {
68
+ try {
69
+ const response = await fetch('/api/sessions/files', {
70
+ headers: { 'x-session-key': key }
71
+ })
72
+ const data = await response.json()
73
+
74
+ if (data.success) {
75
+ setFiles(data.files || [])
76
+ }
77
+ } catch (err) {
78
+ console.error('Failed to load session files:', err)
79
+ }
80
+ }
81
+
82
+ // Load public files
83
+ const loadPublicFiles = async () => {
84
+ try {
85
+ const response = await fetch('/api/sessions/files?public=true')
86
+ const data = await response.json()
87
+
88
+ if (data.success) {
89
+ setPublicFiles(data.files || [])
90
+ }
91
+ } catch (err) {
92
+ console.error('Failed to load public files:', err)
93
+ }
94
+ }
95
+
96
+ // Handle file upload
97
+ const handleFileUpload = async () => {
98
+ if (!uploadFile || !sessionKey) {
99
+ setError('No file selected or session not active')
100
+ return
101
+ }
102
+
103
+ setLoading(true)
104
+ setError(null)
105
+ const formData = new FormData()
106
+ formData.append('file', uploadFile)
107
+ formData.append('public', isPublicUpload.toString())
108
+
109
+ try {
110
+ const response = await fetch('/api/sessions/upload', {
111
+ method: 'POST',
112
+ headers: { 'x-session-key': sessionKey },
113
+ body: formData
114
+ })
115
+ const data = await response.json()
116
+
117
+ if (data.success) {
118
+ setSuccess(`File uploaded successfully: ${data.fileName}`)
119
+ setUploadFile(null)
120
+ if (isPublicUpload) {
121
+ await loadPublicFiles()
122
+ } else {
123
+ await loadSessionFiles(sessionKey)
124
+ }
125
+ } else {
126
+ setError(data.error || 'Failed to upload file')
127
+ }
128
+ } catch (err) {
129
+ setError('Failed to upload file')
130
+ }
131
+ setLoading(false)
132
+ }
133
+
134
+ // Download file
135
+ const downloadFile = async (fileName: string, isPublic: boolean) => {
136
+ const url = `/api/sessions/download?file=${encodeURIComponent(fileName)}${isPublic ? '&public=true' : ''}`
137
+ const headers: HeadersInit = isPublic ? {} : { 'x-session-key': sessionKey }
138
+
139
+ try {
140
+ const response = await fetch(url, { headers })
141
+ if (response.ok) {
142
+ const blob = await response.blob()
143
+ const downloadUrl = window.URL.createObjectURL(blob)
144
+ const a = document.createElement('a')
145
+ a.href = downloadUrl
146
+ a.download = fileName
147
+ document.body.appendChild(a)
148
+ a.click()
149
+ window.URL.revokeObjectURL(downloadUrl)
150
+ document.body.removeChild(a)
151
+ setSuccess(`Downloaded: ${fileName}`)
152
+ } else {
153
+ setError('Failed to download file')
154
+ }
155
+ } catch (err) {
156
+ setError('Failed to download file')
157
+ }
158
+ }
159
+
160
+ // Generate document
161
+ const generateSampleDocument = async (type: string) => {
162
+ if (!sessionKey) {
163
+ setError('No active session')
164
+ return
165
+ }
166
+
167
+ setLoading(true)
168
+ setError(null)
169
+
170
+ const sampleContent: any = {
171
+ docx: {
172
+ type: 'docx',
173
+ fileName: 'sample-document',
174
+ content: {
175
+ title: 'Sample Document',
176
+ content: '# Introduction\n\nThis is a sample document generated by ReubenOS.\n\n## Features\n\n- Session-based isolation\n- Document generation\n- File management\n\n## Conclusion\n\nThank you for using ReubenOS!'
177
+ }
178
+ },
179
+ pdf: {
180
+ type: 'pdf',
181
+ fileName: 'sample-report',
182
+ content: {
183
+ title: 'Sample PDF Report',
184
+ content: 'This is a sample PDF report.\n\n# Section 1\n\nLorem ipsum dolor sit amet.\n\n# Section 2\n\nConclusion and summary.'
185
+ }
186
+ },
187
+ ppt: {
188
+ type: 'ppt',
189
+ fileName: 'sample-presentation',
190
+ content: {
191
+ slides: [
192
+ { title: 'Welcome', content: 'Welcome to ReubenOS', bullets: ['Feature 1', 'Feature 2'] },
193
+ { title: 'Overview', content: 'System Overview', bullets: ['Session Management', 'Document Generation'] },
194
+ { title: 'Thank You', content: 'Questions?' }
195
+ ]
196
+ }
197
+ },
198
+ excel: {
199
+ type: 'excel',
200
+ fileName: 'sample-data',
201
+ content: {
202
+ sheets: [{
203
+ name: 'Sample Data',
204
+ data: {
205
+ headers: ['Name', 'Value', 'Status'],
206
+ rows: [
207
+ ['Item 1', '100', 'Active'],
208
+ ['Item 2', '200', 'Pending'],
209
+ ['Item 3', '150', 'Active']
210
+ ]
211
+ }
212
+ }]
213
+ }
214
+ }
215
+ }
216
+
217
+ const documentData = sampleContent[type]
218
+ if (!documentData) {
219
+ setError('Invalid document type')
220
+ setLoading(false)
221
+ return
222
+ }
223
+
224
+ try {
225
+ const response = await fetch('/api/documents/generate', {
226
+ method: 'POST',
227
+ headers: {
228
+ 'Content-Type': 'application/json',
229
+ 'x-session-key': sessionKey
230
+ },
231
+ body: JSON.stringify({ ...documentData, isPublic: false })
232
+ })
233
+ const data = await response.json()
234
+
235
+ if (data.success) {
236
+ setSuccess(`Generated ${type.toUpperCase()}: ${data.fileName}`)
237
+ await loadSessionFiles(sessionKey)
238
+ } else {
239
+ setError(data.error || `Failed to generate ${type}`)
240
+ }
241
+ } catch (err) {
242
+ setError(`Failed to generate ${type}`)
243
+ }
244
+ setLoading(false)
245
+ }
246
+
247
+ // Copy session key
248
+ const copySessionKey = () => {
249
+ navigator.clipboard.writeText(sessionKey)
250
+ setCopiedKey(true)
251
+ setTimeout(() => setCopiedKey(false), 2000)
252
+ }
253
+
254
+ // Get file icon based on extension
255
+ const getFileIcon = (fileName: string) => {
256
+ const ext = fileName.split('.').pop()?.toLowerCase()
257
+ switch (ext) {
258
+ case 'docx':
259
+ case 'doc':
260
+ return <FileText size={20} weight="fill" className="text-blue-500" />
261
+ case 'xlsx':
262
+ case 'xls':
263
+ return <FileSpreadsheet size={20} weight="fill" className="text-green-500" />
264
+ case 'pptx':
265
+ case 'ppt':
266
+ return <FilePresentation size={20} weight="fill" className="text-orange-500" />
267
+ case 'pdf':
268
+ return <FilePdf size={20} weight="fill" className="text-red-500" />
269
+ default:
270
+ return <File size={20} weight="fill" className="text-gray-500" />
271
+ }
272
+ }
273
+
274
+ // Load files on mount and handle Escape key
275
+ useEffect(() => {
276
+ // Load files for the current session
277
+ if (sessionKey) {
278
+ loadSessionFiles(sessionKey)
279
+ }
280
+ loadPublicFiles()
281
+
282
+ // Handle Escape key to close window
283
+ const handleEscape = (e: KeyboardEvent) => {
284
+ if (e.key === 'Escape') {
285
+ onClose()
286
+ }
287
+ }
288
+ window.addEventListener('keydown', handleEscape)
289
+ return () => window.removeEventListener('keydown', handleEscape)
290
+ }, [onClose, sessionKey])
291
+
292
+ // Clear messages after 3 seconds
293
+ useEffect(() => {
294
+ if (success) {
295
+ const timer = setTimeout(() => setSuccess(null), 3000)
296
+ return () => clearTimeout(timer)
297
+ }
298
+ }, [success])
299
+
300
+ useEffect(() => {
301
+ if (error) {
302
+ const timer = setTimeout(() => setError(null), 3000)
303
+ return () => clearTimeout(timer)
304
+ }
305
+ }, [error])
306
+
307
+ return (
308
+ <Draggable
309
+ handle=".window-header"
310
+ defaultPosition={{ x: 100, y: 100 }}
311
+ disabled={isMaximized}
312
+ nodeRef={nodeRef}
313
+ >
314
+ <div
315
+ ref={nodeRef}
316
+ className={`bg-gray-900/95 backdrop-blur-xl rounded-xl shadow-2xl border border-purple-500/30 overflow-hidden ${
317
+ isMaximized ? 'fixed inset-4' : 'w-[900px]'
318
+ }`}
319
+ style={{ zIndex: 1000 }}
320
+ >
321
+ {/* Window Header */}
322
+ <div className="window-header bg-gradient-to-r from-purple-600/20 to-purple-800/20 p-3 flex items-center justify-between border-b border-purple-500/20 cursor-move">
323
+ <div className="flex items-center gap-2">
324
+ <Key size={20} className="text-purple-400" />
325
+ <span className="text-white font-semibold">Session Manager</span>
326
+ </div>
327
+ <div className="flex items-center gap-1">
328
+ <button
329
+ onClick={(e) => {
330
+ e.stopPropagation();
331
+ setIsMaximized(false);
332
+ }}
333
+ className="w-7 h-7 flex items-center justify-center hover:bg-yellow-500/20 rounded-md transition-colors group"
334
+ title="Minimize"
335
+ >
336
+ <Minus size={14} className="text-yellow-400 group-hover:text-yellow-300" />
337
+ </button>
338
+ <button
339
+ onClick={(e) => {
340
+ e.stopPropagation();
341
+ setIsMaximized(!isMaximized);
342
+ }}
343
+ className="w-7 h-7 flex items-center justify-center hover:bg-green-500/20 rounded-md transition-colors group"
344
+ title={isMaximized ? "Restore" : "Maximize"}
345
+ >
346
+ <Square size={14} className="text-green-400 group-hover:text-green-300" />
347
+ </button>
348
+ <button
349
+ onClick={(e) => {
350
+ e.stopPropagation();
351
+ onClose();
352
+ }}
353
+ className="w-7 h-7 flex items-center justify-center hover:bg-red-500/30 rounded-md transition-colors group"
354
+ title="Close Window"
355
+ >
356
+ <X size={14} className="text-red-400 group-hover:text-red-300" />
357
+ </button>
358
+ </div>
359
+ </div>
360
+
361
+ {/* Window Content */}
362
+ <div className={`p-6 ${isMaximized ? 'h-[calc(100%-50px)] overflow-auto' : 'h-[600px] overflow-auto'}`}>
363
+ {/* Session Info */}
364
+ <div className="mb-6">
365
+ <div>
366
+ <div className="flex items-center justify-between mb-4">
367
+ <h2 className="text-xl font-semibold text-white">Active Session</h2>
368
+ </div>
369
+ <div className="space-y-3">
370
+ <div className="bg-black/30 rounded-lg p-3 border border-purple-500/30">
371
+ <div className="flex items-center justify-between mb-2">
372
+ <span className="text-gray-400 text-sm">Session Key:</span>
373
+ <button
374
+ onClick={copySessionKey}
375
+ className="flex items-center gap-2 px-3 py-1 bg-purple-600/20 hover:bg-purple-600/30 rounded-lg transition-colors"
376
+ >
377
+ {copiedKey ? (
378
+ <>
379
+ <Check size={16} className="text-green-400" />
380
+ <span className="text-green-400 text-sm">Copied!</span>
381
+ </>
382
+ ) : (
383
+ <>
384
+ <Copy size={16} className="text-purple-400" />
385
+ <span className="text-purple-400 text-sm">Copy Full Key</span>
386
+ </>
387
+ )}
388
+ </button>
389
+ </div>
390
+ <div className="flex items-center gap-2">
391
+ <input
392
+ type="text"
393
+ value={sessionKey}
394
+ readOnly
395
+ onClick={(e) => e.currentTarget.select()}
396
+ className="flex-1 bg-black/50 text-purple-300 text-xs px-2 py-1 rounded border border-purple-500/20 font-mono"
397
+ />
398
+ </div>
399
+ </div>
400
+ <div className="bg-black/30 rounded-lg p-3 border border-purple-500/30">
401
+ <div className="flex items-center justify-between">
402
+ <div className="flex items-center gap-2">
403
+ <span className="text-gray-400 text-sm">Session ID:</span>
404
+ <span className="text-white font-mono text-xs bg-black/50 px-2 py-1 rounded">{sessionId}</span>
405
+ </div>
406
+ <button
407
+ onClick={() => {
408
+ navigator.clipboard.writeText(sessionId);
409
+ setCopiedId(true);
410
+ setTimeout(() => setCopiedId(false), 2000);
411
+ }}
412
+ className="flex items-center gap-1 px-2 py-1 bg-purple-600/20 hover:bg-purple-600/30 rounded transition-colors"
413
+ >
414
+ {copiedId ? (
415
+ <>
416
+ <Check size={14} className="text-green-400" />
417
+ <span className="text-green-400 text-xs">Copied!</span>
418
+ </>
419
+ ) : (
420
+ <>
421
+ <Copy size={14} className="text-purple-400" />
422
+ <span className="text-purple-400 text-xs">Copy ID</span>
423
+ </>
424
+ )}
425
+ </button>
426
+ </div>
427
+ </div>
428
+ </div>
429
+ </div>
430
+ </div>
431
+
432
+ {/* File Upload */}
433
+ <div className="mb-6">
434
+ <h3 className="text-lg font-semibold text-white mb-4">Upload File</h3>
435
+ <div className="flex gap-4">
436
+ <input
437
+ type="file"
438
+ onChange={(e) => setUploadFile(e.target.files?.[0] || null)}
439
+ className="flex-1 text-white file:mr-4 file:py-2 file:px-4 file:rounded-lg file:border-0 file:bg-purple-600 file:text-white hover:file:bg-purple-700"
440
+ />
441
+ <label className="flex items-center gap-2 text-white">
442
+ <input
443
+ type="checkbox"
444
+ checked={isPublicUpload}
445
+ onChange={(e) => setIsPublicUpload(e.target.checked)}
446
+ className="rounded"
447
+ />
448
+ Public
449
+ </label>
450
+ <button
451
+ onClick={handleFileUpload}
452
+ disabled={!uploadFile || loading}
453
+ className="px-4 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700 transition-colors disabled:opacity-50"
454
+ >
455
+ <Upload size={20} className="inline mr-2" />
456
+ Upload
457
+ </button>
458
+ </div>
459
+ </div>
460
+
461
+ {/* Document Generation */}
462
+ <div className="mb-6">
463
+ <h3 className="text-lg font-semibold text-white mb-4">Generate Documents</h3>
464
+ <div className="grid grid-cols-4 gap-4">
465
+ <button
466
+ onClick={() => generateSampleDocument('docx')}
467
+ disabled={loading}
468
+ className="p-4 bg-blue-600/20 border border-blue-500/30 rounded-lg hover:bg-blue-600/30 transition-colors disabled:opacity-50"
469
+ >
470
+ <FileText size={32} className="text-blue-400 mx-auto mb-2" />
471
+ <span className="text-white text-sm">Word Doc</span>
472
+ </button>
473
+ <button
474
+ onClick={() => generateSampleDocument('pdf')}
475
+ disabled={loading}
476
+ className="p-4 bg-red-600/20 border border-red-500/30 rounded-lg hover:bg-red-600/30 transition-colors disabled:opacity-50"
477
+ >
478
+ <FilePdf size={32} className="text-red-400 mx-auto mb-2" />
479
+ <span className="text-white text-sm">PDF</span>
480
+ </button>
481
+ <button
482
+ onClick={() => generateSampleDocument('ppt')}
483
+ disabled={loading}
484
+ className="p-4 bg-orange-600/20 border border-orange-500/30 rounded-lg hover:bg-orange-600/30 transition-colors disabled:opacity-50"
485
+ >
486
+ <FilePresentation size={32} className="text-orange-400 mx-auto mb-2" />
487
+ <span className="text-white text-sm">PowerPoint</span>
488
+ </button>
489
+ <button
490
+ onClick={() => generateSampleDocument('excel')}
491
+ disabled={loading}
492
+ className="p-4 bg-green-600/20 border border-green-500/30 rounded-lg hover:bg-green-600/30 transition-colors disabled:opacity-50"
493
+ >
494
+ <FileSpreadsheet size={32} className="text-green-400 mx-auto mb-2" />
495
+ <span className="text-white text-sm">Excel</span>
496
+ </button>
497
+ </div>
498
+ </div>
499
+
500
+ {/* Files List */}
501
+ <div>
502
+ {/* Tabs */}
503
+ <div className="flex gap-4 mb-4">
504
+ <button
505
+ onClick={() => setActiveTab('session')}
506
+ className={`px-4 py-2 rounded-lg transition-colors flex items-center gap-2 ${
507
+ activeTab === 'session'
508
+ ? 'bg-purple-600 text-white'
509
+ : 'bg-gray-800 text-gray-400 hover:bg-gray-700'
510
+ }`}
511
+ >
512
+ <Lock size={20} />
513
+ Session Files ({files.length})
514
+ </button>
515
+ <button
516
+ onClick={() => setActiveTab('public')}
517
+ className={`px-4 py-2 rounded-lg transition-colors flex items-center gap-2 ${
518
+ activeTab === 'public'
519
+ ? 'bg-purple-600 text-white'
520
+ : 'bg-gray-800 text-gray-400 hover:bg-gray-700'
521
+ }`}
522
+ >
523
+ <Globe size={20} />
524
+ Public Files ({publicFiles.length})
525
+ </button>
526
+ <button
527
+ onClick={() => {
528
+ if (activeTab === 'session' && sessionKey) {
529
+ loadSessionFiles(sessionKey)
530
+ } else {
531
+ loadPublicFiles()
532
+ }
533
+ }}
534
+ className="ml-auto p-2 bg-gray-800 text-gray-400 rounded-lg hover:bg-gray-700 transition-colors"
535
+ >
536
+ <RefreshCw size={20} />
537
+ </button>
538
+ </div>
539
+
540
+ {/* File List */}
541
+ <div className="space-y-2">
542
+ {(activeTab === 'session' ? files : publicFiles).map((file) => (
543
+ <div
544
+ key={file.name}
545
+ className="flex items-center justify-between p-3 bg-gray-800/50 rounded-lg hover:bg-gray-800/70 transition-colors"
546
+ >
547
+ <div className="flex items-center gap-3">
548
+ {getFileIcon(file.name)}
549
+ <div>
550
+ <p className="text-white font-medium">{file.name}</p>
551
+ <p className="text-gray-400 text-sm">
552
+ {(file.size / 1024).toFixed(2)} KB • {new Date(file.modified).toLocaleDateString()}
553
+ </p>
554
+ </div>
555
+ </div>
556
+ <button
557
+ onClick={() => downloadFile(file.name, activeTab === 'public')}
558
+ className="p-2 bg-purple-600/20 text-purple-400 rounded-lg hover:bg-purple-600/30 transition-colors"
559
+ >
560
+ <Download size={20} />
561
+ </button>
562
+ </div>
563
+ ))}
564
+ {(activeTab === 'session' ? files : publicFiles).length === 0 && (
565
+ <div className="text-center py-8">
566
+ <Folder size={48} className="text-gray-600 mx-auto mb-4" />
567
+ <p className="text-gray-400">No files in {activeTab === 'session' ? 'session' : 'public'} folder</p>
568
+ </div>
569
+ )}
570
+ </div>
571
+ </div>
572
+ </div>
573
+
574
+ {/* Messages */}
575
+ <AnimatePresence>
576
+ {success && (
577
+ <motion.div
578
+ initial={{ opacity: 0, y: 50 }}
579
+ animate={{ opacity: 1, y: 0 }}
580
+ exit={{ opacity: 0, y: 50 }}
581
+ className="absolute bottom-4 right-4 px-4 py-2 bg-green-600 text-white rounded-lg shadow-lg"
582
+ >
583
+ <Check size={16} className="inline mr-2" />
584
+ {success}
585
+ </motion.div>
586
+ )}
587
+ {error && (
588
+ <motion.div
589
+ initial={{ opacity: 0, y: 50 }}
590
+ animate={{ opacity: 1, y: 0 }}
591
+ exit={{ opacity: 0, y: 50 }}
592
+ className="absolute bottom-4 right-4 px-4 py-2 bg-red-600 text-white rounded-lg shadow-lg"
593
+ >
594
+ <X size={16} className="inline mr-2" />
595
+ {error}
596
+ </motion.div>
597
+ )}
598
+ </AnimatePresence>
599
+ </div>
600
+ </Draggable>
601
+ )
602
+ }
app/components/VSCodeEditor.tsx DELETED
@@ -1,458 +0,0 @@
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/sessions/page.tsx ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ import { SessionManager } from '../components/SessionManager'
2
+
3
+ export default function SessionsPage() {
4
+ return <SessionManager />
5
+ }
lib/documentGenerators.ts ADDED
@@ -0,0 +1,380 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Document, Packer, Paragraph, TextRun, HeadingLevel, AlignmentType, Table, TableRow, TableCell, WidthType } from 'docx';
2
+ import PDFDocument from 'pdfkit';
3
+ import ExcelJS from 'exceljs';
4
+ // @ts-ignore
5
+ import officegen from 'officegen';
6
+ import fs from 'fs';
7
+ import path from 'path';
8
+
9
+ export interface DocumentContent {
10
+ title?: string;
11
+ content: string | any[];
12
+ metadata?: Record<string, any>;
13
+ }
14
+
15
+ export interface TableData {
16
+ headers: string[];
17
+ rows: string[][];
18
+ }
19
+
20
+ export interface SlideContent {
21
+ title: string;
22
+ content: string | string[];
23
+ bullets?: string[];
24
+ }
25
+
26
+ export class DocumentGenerator {
27
+ // Generate DOCX file
28
+ static async generateDOCX(content: DocumentContent): Promise<Buffer> {
29
+ const doc = new Document({
30
+ sections: [{
31
+ properties: {},
32
+ children: this.parseContentToParagraphs(content.content, content.title)
33
+ }]
34
+ });
35
+
36
+ return await Packer.toBuffer(doc);
37
+ }
38
+
39
+ private static parseContentToParagraphs(content: string | any, title?: string): Paragraph[] {
40
+ const paragraphs: Paragraph[] = [];
41
+
42
+ // Add title if provided
43
+ if (title) {
44
+ paragraphs.push(new Paragraph({
45
+ text: title,
46
+ heading: HeadingLevel.HEADING_1,
47
+ spacing: { after: 200 }
48
+ }));
49
+ }
50
+
51
+ // Handle complex nested structures (like the letter example)
52
+ if (typeof content === 'object' && content !== null) {
53
+ // Check if it has sections array
54
+ if (content.sections && Array.isArray(content.sections)) {
55
+ for (const section of content.sections) {
56
+ if (section.content) {
57
+ // Add section content
58
+ if (typeof section.content === 'string') {
59
+ const lines = section.content.split('\n');
60
+ for (const line of lines) {
61
+ if (line.trim()) {
62
+ paragraphs.push(new Paragraph({
63
+ children: [new TextRun(line)],
64
+ spacing: { after: 100 }
65
+ }));
66
+ }
67
+ }
68
+ }
69
+ }
70
+
71
+ // Handle paragraphs array in section
72
+ if (section.paragraphs && Array.isArray(section.paragraphs)) {
73
+ for (const para of section.paragraphs) {
74
+ paragraphs.push(new Paragraph({
75
+ children: [new TextRun(para)],
76
+ spacing: { after: 100 }
77
+ }));
78
+ }
79
+ }
80
+ }
81
+ return paragraphs;
82
+ }
83
+
84
+ // Handle direct content field
85
+ if (content.content) {
86
+ return this.parseContentToParagraphs(content.content, title);
87
+ }
88
+ }
89
+
90
+ if (typeof content === 'string') {
91
+ const lines = content.split('\n');
92
+ for (const line of lines) {
93
+ if (line.startsWith('# ')) {
94
+ // H1 heading
95
+ paragraphs.push(new Paragraph({
96
+ text: line.substring(2),
97
+ heading: HeadingLevel.HEADING_1
98
+ }));
99
+ } else if (line.startsWith('## ')) {
100
+ // H2 heading
101
+ paragraphs.push(new Paragraph({
102
+ text: line.substring(3),
103
+ heading: HeadingLevel.HEADING_2
104
+ }));
105
+ } else if (line.startsWith('### ')) {
106
+ // H3 heading
107
+ paragraphs.push(new Paragraph({
108
+ text: line.substring(4),
109
+ heading: HeadingLevel.HEADING_3
110
+ }));
111
+ } else if (line.startsWith('- ') || line.startsWith('* ')) {
112
+ // Bullet point
113
+ paragraphs.push(new Paragraph({
114
+ text: line.substring(2),
115
+ bullet: {
116
+ level: 0
117
+ }
118
+ }));
119
+ } else if (line.startsWith('1. ') || /^\d+\. /.test(line)) {
120
+ // Numbered list
121
+ paragraphs.push(new Paragraph({
122
+ text: line.replace(/^\d+\. /, ''),
123
+ numbering: {
124
+ reference: "default-numbering",
125
+ level: 0
126
+ }
127
+ }));
128
+ } else {
129
+ // Regular paragraph
130
+ paragraphs.push(new Paragraph({
131
+ children: [new TextRun(line)]
132
+ }));
133
+ }
134
+ }
135
+ } else if (Array.isArray(content)) {
136
+ for (const item of content) {
137
+ if (typeof item === 'string') {
138
+ paragraphs.push(new Paragraph({
139
+ children: [new TextRun(item)]
140
+ }));
141
+ } else if (item.type === 'heading') {
142
+ paragraphs.push(new Paragraph({
143
+ text: item.text,
144
+ heading: item.level || HeadingLevel.HEADING_1
145
+ }));
146
+ } else if (item.type === 'paragraph') {
147
+ paragraphs.push(new Paragraph({
148
+ children: [new TextRun(item.text)]
149
+ }));
150
+ } else if (item.type === 'bullet') {
151
+ paragraphs.push(new Paragraph({
152
+ text: item.text,
153
+ bullet: {
154
+ level: item.level || 0
155
+ }
156
+ }));
157
+ }
158
+ }
159
+ }
160
+
161
+ return paragraphs;
162
+ }
163
+
164
+ // Generate PDF file
165
+ static async generatePDF(content: DocumentContent): Promise<Buffer> {
166
+ return new Promise((resolve, reject) => {
167
+ const doc = new PDFDocument();
168
+ const buffers: Buffer[] = [];
169
+
170
+ doc.on('data', buffers.push.bind(buffers));
171
+ doc.on('end', () => {
172
+ const pdfData = Buffer.concat(buffers);
173
+ resolve(pdfData);
174
+ });
175
+ doc.on('error', reject);
176
+
177
+ // Add title if provided
178
+ if (content.title) {
179
+ doc.fontSize(20).text(content.title, { align: 'center' });
180
+ doc.moveDown();
181
+ }
182
+
183
+ // Add content
184
+ if (typeof content.content === 'string') {
185
+ const lines = content.content.split('\n');
186
+ for (const line of lines) {
187
+ if (line.startsWith('# ')) {
188
+ doc.fontSize(18).text(line.substring(2));
189
+ } else if (line.startsWith('## ')) {
190
+ doc.fontSize(16).text(line.substring(3));
191
+ } else if (line.startsWith('### ')) {
192
+ doc.fontSize(14).text(line.substring(4));
193
+ } else if (line.startsWith('- ') || line.startsWith('* ')) {
194
+ doc.fontSize(12).text(`• ${line.substring(2)}`, { indent: 20 });
195
+ } else {
196
+ doc.fontSize(12).text(line);
197
+ }
198
+ doc.moveDown(0.5);
199
+ }
200
+ }
201
+
202
+ doc.end();
203
+ });
204
+ }
205
+
206
+ // Generate PowerPoint file
207
+ static async generatePowerPoint(slides: SlideContent[]): Promise<Buffer> {
208
+ return new Promise((resolve, reject) => {
209
+ const pptx = officegen('pptx');
210
+ const buffers: Buffer[] = [];
211
+
212
+ pptx.on('data', (data: Buffer) => buffers.push(data));
213
+ pptx.on('end', () => {
214
+ const pptData = Buffer.concat(buffers);
215
+ resolve(pptData);
216
+ });
217
+ pptx.on('error', reject);
218
+
219
+ // Add title slide
220
+ if (slides.length > 0 && slides[0].title) {
221
+ const titleSlide = pptx.makeNewSlide();
222
+ titleSlide.title = slides[0].title;
223
+ if (slides[0].content) {
224
+ titleSlide.addText(slides[0].content.toString(), {
225
+ x: 'c',
226
+ y: 'c',
227
+ cx: '80%',
228
+ cy: '30%',
229
+ font_size: 18
230
+ });
231
+ }
232
+ }
233
+
234
+ // Add content slides
235
+ for (let i = 1; i < slides.length; i++) {
236
+ const slide = pptx.makeNewSlide();
237
+ slide.title = slides[i].title;
238
+
239
+ if (slides[i].bullets && slides[i].bullets!.length > 0) {
240
+ // Add bullet points
241
+ const bullets = slides[i].bullets!.map(bullet => ({
242
+ text: bullet,
243
+ options: { font_size: 14 }
244
+ }));
245
+ slide.addText(bullets, {
246
+ x: 0.5,
247
+ y: 1.5,
248
+ cx: '90%',
249
+ cy: '70%'
250
+ });
251
+ } else if (slides[i].content) {
252
+ // Add regular content
253
+ slide.addText(slides[i].content.toString(), {
254
+ x: 0.5,
255
+ y: 1.5,
256
+ cx: '90%',
257
+ cy: '70%',
258
+ font_size: 14
259
+ });
260
+ }
261
+ }
262
+
263
+ pptx.generate();
264
+ });
265
+ }
266
+
267
+ // Generate Excel file
268
+ static async generateExcel(data: { sheets: { name: string; data: TableData }[] }): Promise<Buffer> {
269
+ const workbook = new ExcelJS.Workbook();
270
+
271
+ workbook.creator = 'ReubenOS';
272
+ workbook.created = new Date();
273
+ workbook.modified = new Date();
274
+
275
+ for (const sheetData of data.sheets) {
276
+ const worksheet = workbook.addWorksheet(sheetData.name);
277
+
278
+ // Add headers
279
+ worksheet.addRow(sheetData.data.headers);
280
+
281
+ // Style headers
282
+ worksheet.getRow(1).font = { bold: true };
283
+ worksheet.getRow(1).fill = {
284
+ type: 'pattern',
285
+ pattern: 'solid',
286
+ fgColor: { argb: 'FFE0E0E0' }
287
+ };
288
+
289
+ // Add data rows
290
+ for (const row of sheetData.data.rows) {
291
+ worksheet.addRow(row);
292
+ }
293
+
294
+ // Auto-fit columns
295
+ worksheet.columns.forEach((column) => {
296
+ let maxLength = 0;
297
+ if (column && column.eachCell) {
298
+ column.eachCell({ includeEmpty: true }, (cell) => {
299
+ const length = cell.value ? cell.value.toString().length : 10;
300
+ if (length > maxLength) {
301
+ maxLength = length;
302
+ }
303
+ });
304
+ column.width = maxLength + 2;
305
+ }
306
+ });
307
+
308
+ // Add borders to all cells with data
309
+ const rowCount = worksheet.rowCount;
310
+ const colCount = worksheet.columnCount;
311
+ for (let row = 1; row <= rowCount; row++) {
312
+ for (let col = 1; col <= colCount; col++) {
313
+ const cell = worksheet.getCell(row, col);
314
+ cell.border = {
315
+ top: { style: 'thin' },
316
+ left: { style: 'thin' },
317
+ bottom: { style: 'thin' },
318
+ right: { style: 'thin' }
319
+ };
320
+ }
321
+ }
322
+ }
323
+
324
+ const buffer = await workbook.xlsx.writeBuffer();
325
+ return Buffer.from(buffer);
326
+ }
327
+
328
+ // Generate LaTeX and compile to PDF (requires latex installation)
329
+ static async generateLatexPDF(latexContent: string): Promise<Buffer> {
330
+ // For now, we'll use PDFKit as LaTeX compilation requires external tools
331
+ // In production, you'd use node-latex or similar with a LaTeX installation
332
+ return this.generatePDF({
333
+ content: this.convertLatexToPlainText(latexContent)
334
+ });
335
+ }
336
+
337
+ private static convertLatexToPlainText(latex: string): string {
338
+ // Basic LaTeX to plain text conversion
339
+ return latex
340
+ .replace(/\\documentclass{.*?}/g, '')
341
+ .replace(/\\usepackage{.*?}/g, '')
342
+ .replace(/\\begin{document}/g, '')
343
+ .replace(/\\end{document}/g, '')
344
+ .replace(/\\section{(.*?)}/g, '# $1')
345
+ .replace(/\\subsection{(.*?)}/g, '## $1')
346
+ .replace(/\\subsubsection{(.*?)}/g, '### $1')
347
+ .replace(/\\textbf{(.*?)}/g, '**$1**')
348
+ .replace(/\\textit{(.*?)}/g, '*$1*')
349
+ .replace(/\\item/g, '- ')
350
+ .replace(/\\begin{itemize}/g, '')
351
+ .replace(/\\end{itemize}/g, '')
352
+ .replace(/\\begin{enumerate}/g, '')
353
+ .replace(/\\end{enumerate}/g, '')
354
+ .replace(/\$/g, '')
355
+ .replace(/\\/g, '\n')
356
+ .trim();
357
+ }
358
+
359
+ // Convert markdown to structured content for document generation
360
+ static parseMarkdown(markdown: string): any[] {
361
+ const lines = markdown.split('\n');
362
+ const content = [];
363
+
364
+ for (const line of lines) {
365
+ if (line.startsWith('# ')) {
366
+ content.push({ type: 'heading', text: line.substring(2), level: HeadingLevel.HEADING_1 });
367
+ } else if (line.startsWith('## ')) {
368
+ content.push({ type: 'heading', text: line.substring(3), level: HeadingLevel.HEADING_2 });
369
+ } else if (line.startsWith('### ')) {
370
+ content.push({ type: 'heading', text: line.substring(4), level: HeadingLevel.HEADING_3 });
371
+ } else if (line.startsWith('- ') || line.startsWith('* ')) {
372
+ content.push({ type: 'bullet', text: line.substring(2) });
373
+ } else if (line.trim() !== '') {
374
+ content.push({ type: 'paragraph', text: line });
375
+ }
376
+ }
377
+
378
+ return content;
379
+ }
380
+ }
lib/sessionManager.ts ADDED
@@ -0,0 +1,207 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import crypto from 'crypto';
2
+ import fs from 'fs/promises';
3
+ import path from 'path';
4
+
5
+ export interface Session {
6
+ id: string;
7
+ key: string;
8
+ createdAt: Date;
9
+ lastAccessed: Date;
10
+ metadata?: Record<string, any>;
11
+ }
12
+
13
+ export class SessionManager {
14
+ private static instance: SessionManager;
15
+ private sessions: Map<string, Session> = new Map();
16
+ private sessionDir: string;
17
+ private publicDir: string;
18
+
19
+ private constructor() {
20
+ this.sessionDir = path.join(process.cwd(), 'data', 'files');
21
+ this.publicDir = path.join(process.cwd(), 'data', 'public');
22
+ this.initializeDirectories();
23
+ }
24
+
25
+ static getInstance(): SessionManager {
26
+ if (!SessionManager.instance) {
27
+ SessionManager.instance = new SessionManager();
28
+ }
29
+ return SessionManager.instance;
30
+ }
31
+
32
+ private async initializeDirectories() {
33
+ try {
34
+ await fs.mkdir(this.sessionDir, { recursive: true });
35
+ await fs.mkdir(this.publicDir, { recursive: true });
36
+ } catch (error) {
37
+ console.error('Error initializing directories:', error);
38
+ }
39
+ }
40
+
41
+ generateSessionKey(): string {
42
+ return crypto.randomBytes(32).toString('hex');
43
+ }
44
+
45
+ async createSession(metadata?: Record<string, any>): Promise<Session> {
46
+ const sessionId = `session_${Date.now()}_${crypto.randomBytes(8).toString('hex')}`;
47
+ const sessionKey = this.generateSessionKey();
48
+
49
+ const session: Session = {
50
+ id: sessionId,
51
+ key: sessionKey,
52
+ createdAt: new Date(),
53
+ lastAccessed: new Date(),
54
+ metadata
55
+ };
56
+
57
+ this.sessions.set(sessionKey, session);
58
+
59
+ // Create session directory
60
+ const sessionPath = path.join(this.sessionDir, sessionId);
61
+ await fs.mkdir(sessionPath, { recursive: true });
62
+
63
+ // Save session metadata
64
+ await fs.writeFile(
65
+ path.join(sessionPath, 'session.json'),
66
+ JSON.stringify(session, null, 2)
67
+ );
68
+
69
+ return session;
70
+ }
71
+
72
+ async validateSession(sessionKey: string): Promise<boolean> {
73
+ if (this.sessions.has(sessionKey)) {
74
+ const session = this.sessions.get(sessionKey)!;
75
+ session.lastAccessed = new Date();
76
+ return true;
77
+ }
78
+
79
+ // Check if session exists on disk
80
+ try {
81
+ const sessionsOnDisk = await fs.readdir(this.sessionDir);
82
+ for (const sessionId of sessionsOnDisk) {
83
+ const sessionPath = path.join(this.sessionDir, sessionId, 'session.json');
84
+ try {
85
+ const sessionData = await fs.readFile(sessionPath, 'utf-8');
86
+ const session = JSON.parse(sessionData) as Session;
87
+ if (session.key === sessionKey) {
88
+ session.lastAccessed = new Date();
89
+ this.sessions.set(sessionKey, session);
90
+ return true;
91
+ }
92
+ } catch (error) {
93
+ continue;
94
+ }
95
+ }
96
+ } catch (error) {
97
+ console.error('Error validating session:', error);
98
+ }
99
+
100
+ return false;
101
+ }
102
+
103
+ async getSession(sessionKey: string): Promise<Session | null> {
104
+ if (await this.validateSession(sessionKey)) {
105
+ return this.sessions.get(sessionKey) || null;
106
+ }
107
+ return null;
108
+ }
109
+
110
+ async getSessionPath(sessionKey: string): Promise<string | null> {
111
+ const session = await this.getSession(sessionKey);
112
+ if (session) {
113
+ return path.join(this.sessionDir, session.id);
114
+ }
115
+ return null;
116
+ }
117
+
118
+ getPublicPath(): string {
119
+ return this.publicDir;
120
+ }
121
+
122
+ async listSessionFiles(sessionKey: string): Promise<string[]> {
123
+ const sessionPath = await this.getSessionPath(sessionKey);
124
+ if (!sessionPath) {
125
+ throw new Error('Invalid session key');
126
+ }
127
+
128
+ try {
129
+ const files = await fs.readdir(sessionPath);
130
+ return files.filter(file => file !== 'session.json');
131
+ } catch (error) {
132
+ console.error('Error listing session files:', error);
133
+ return [];
134
+ }
135
+ }
136
+
137
+ async saveFileToSession(sessionKey: string, fileName: string, content: Buffer | string): Promise<string> {
138
+ const sessionPath = await this.getSessionPath(sessionKey);
139
+ if (!sessionPath) {
140
+ throw new Error('Invalid session key');
141
+ }
142
+
143
+ const filePath = path.join(sessionPath, fileName);
144
+ await fs.writeFile(filePath, content);
145
+ return filePath;
146
+ }
147
+
148
+ async getFileFromSession(sessionKey: string, fileName: string): Promise<Buffer> {
149
+ const sessionPath = await this.getSessionPath(sessionKey);
150
+ if (!sessionPath) {
151
+ throw new Error('Invalid session key');
152
+ }
153
+
154
+ const filePath = path.join(sessionPath, fileName);
155
+ return await fs.readFile(filePath);
156
+ }
157
+
158
+ async saveFileToPublic(fileName: string, content: Buffer | string): Promise<string> {
159
+ const filePath = path.join(this.publicDir, fileName);
160
+ await fs.writeFile(filePath, content);
161
+ return filePath;
162
+ }
163
+
164
+ async getFileFromPublic(fileName: string): Promise<Buffer> {
165
+ const filePath = path.join(this.publicDir, fileName);
166
+ return await fs.readFile(filePath);
167
+ }
168
+
169
+ async deleteSession(sessionKey: string): Promise<boolean> {
170
+ const sessionPath = await this.getSessionPath(sessionKey);
171
+ if (!sessionPath) {
172
+ return false;
173
+ }
174
+
175
+ try {
176
+ await fs.rm(sessionPath, { recursive: true, force: true });
177
+ this.sessions.delete(sessionKey);
178
+ return true;
179
+ } catch (error) {
180
+ console.error('Error deleting session:', error);
181
+ return false;
182
+ }
183
+ }
184
+
185
+ // Clean up old sessions (older than 24 hours)
186
+ async cleanupOldSessions(): Promise<void> {
187
+ const oneDayAgo = new Date(Date.now() - 24 * 60 * 60 * 1000);
188
+
189
+ try {
190
+ const sessionsOnDisk = await fs.readdir(this.sessionDir);
191
+ for (const sessionId of sessionsOnDisk) {
192
+ const sessionPath = path.join(this.sessionDir, sessionId, 'session.json');
193
+ try {
194
+ const sessionData = await fs.readFile(sessionPath, 'utf-8');
195
+ const session = JSON.parse(sessionData) as Session;
196
+ if (new Date(session.lastAccessed) < oneDayAgo) {
197
+ await fs.rm(path.join(this.sessionDir, sessionId), { recursive: true, force: true });
198
+ }
199
+ } catch (error) {
200
+ continue;
201
+ }
202
+ }
203
+ } catch (error) {
204
+ console.error('Error cleaning up old sessions:', error);
205
+ }
206
+ }
207
+ }
mcp-config.json ADDED
@@ -0,0 +1,234 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "ReubenOS File Manager",
3
+ "description": "MCP integration for ReubenOS file management with session-based isolation",
4
+ "version": "1.0.0",
5
+ "baseUrl": "http://localhost:3000",
6
+ "tools": [
7
+ {
8
+ "name": "createSession",
9
+ "description": "Create a new isolated session with a unique key",
10
+ "endpoint": "/api/sessions/create",
11
+ "method": "POST",
12
+ "parameters": {
13
+ "metadata": {
14
+ "type": "object",
15
+ "description": "Optional metadata for the session",
16
+ "optional": true
17
+ }
18
+ },
19
+ "returns": {
20
+ "id": "Session ID",
21
+ "key": "Session key (required for all operations)",
22
+ "createdAt": "Creation timestamp"
23
+ }
24
+ },
25
+ {
26
+ "name": "uploadFile",
27
+ "description": "Upload a file to session or public folder",
28
+ "endpoint": "/api/sessions/upload",
29
+ "method": "POST",
30
+ "headers": {
31
+ "x-session-key": "required"
32
+ },
33
+ "parameters": {
34
+ "file": {
35
+ "type": "file",
36
+ "description": "File to upload",
37
+ "required": true
38
+ },
39
+ "public": {
40
+ "type": "boolean",
41
+ "description": "Save to public folder (default: false)",
42
+ "optional": true
43
+ }
44
+ }
45
+ },
46
+ {
47
+ "name": "downloadFile",
48
+ "description": "Download a file from session or public folder",
49
+ "endpoint": "/api/sessions/download",
50
+ "method": "GET",
51
+ "headers": {
52
+ "x-session-key": "required for private files"
53
+ },
54
+ "parameters": {
55
+ "file": {
56
+ "type": "string",
57
+ "description": "File name to download",
58
+ "required": true
59
+ },
60
+ "public": {
61
+ "type": "boolean",
62
+ "description": "Download from public folder",
63
+ "optional": true
64
+ }
65
+ }
66
+ },
67
+ {
68
+ "name": "listFiles",
69
+ "description": "List files in session or public folder",
70
+ "endpoint": "/api/sessions/files",
71
+ "method": "GET",
72
+ "headers": {
73
+ "x-session-key": "required for session files"
74
+ },
75
+ "parameters": {
76
+ "public": {
77
+ "type": "boolean",
78
+ "description": "List public files",
79
+ "optional": true
80
+ }
81
+ }
82
+ },
83
+ {
84
+ "name": "generateDocument",
85
+ "description": "Generate DOCX, PDF, PowerPoint, or Excel documents",
86
+ "endpoint": "/api/documents/generate",
87
+ "method": "POST",
88
+ "headers": {
89
+ "x-session-key": "required"
90
+ },
91
+ "parameters": {
92
+ "type": {
93
+ "type": "string",
94
+ "description": "Document type: docx, pdf, latex, ppt, excel",
95
+ "required": true
96
+ },
97
+ "fileName": {
98
+ "type": "string",
99
+ "description": "Output file name",
100
+ "required": true
101
+ },
102
+ "content": {
103
+ "type": "object",
104
+ "description": "Document content (structure varies by type)",
105
+ "required": true
106
+ },
107
+ "isPublic": {
108
+ "type": "boolean",
109
+ "description": "Save to public folder",
110
+ "optional": true
111
+ }
112
+ },
113
+ "examples": {
114
+ "docx": {
115
+ "type": "docx",
116
+ "fileName": "report",
117
+ "content": {
118
+ "title": "Monthly Report",
119
+ "content": "# Executive Summary\\n\\nThis is the report content..."
120
+ }
121
+ },
122
+ "powerpoint": {
123
+ "type": "ppt",
124
+ "fileName": "presentation",
125
+ "content": {
126
+ "slides": [
127
+ {
128
+ "title": "Introduction",
129
+ "content": "Welcome to our presentation",
130
+ "bullets": ["Point 1", "Point 2", "Point 3"]
131
+ },
132
+ {
133
+ "title": "Main Content",
134
+ "content": "Detailed information here"
135
+ }
136
+ ]
137
+ }
138
+ },
139
+ "excel": {
140
+ "type": "excel",
141
+ "fileName": "data",
142
+ "content": {
143
+ "sheets": [
144
+ {
145
+ "name": "Sales Data",
146
+ "data": {
147
+ "headers": ["Product", "Q1", "Q2", "Q3", "Q4"],
148
+ "rows": [
149
+ ["Product A", "100", "150", "200", "180"],
150
+ ["Product B", "80", "90", "110", "120"]
151
+ ]
152
+ }
153
+ }
154
+ ]
155
+ }
156
+ }
157
+ }
158
+ },
159
+ {
160
+ "name": "processDocument",
161
+ "description": "Read and analyze documents (DOCX, Excel, PDF, etc.)",
162
+ "endpoint": "/api/documents/process",
163
+ "method": "POST",
164
+ "headers": {
165
+ "x-session-key": "required"
166
+ },
167
+ "parameters": {
168
+ "fileName": {
169
+ "type": "string",
170
+ "description": "File name to process",
171
+ "required": true
172
+ },
173
+ "isPublic": {
174
+ "type": "boolean",
175
+ "description": "File is in public folder",
176
+ "optional": true
177
+ },
178
+ "operation": {
179
+ "type": "string",
180
+ "description": "Operation: read, analyze",
181
+ "optional": true
182
+ }
183
+ }
184
+ }
185
+ ],
186
+ "sessionManagement": {
187
+ "description": "Each user gets a unique session key that provides isolated file storage",
188
+ "important": [
189
+ "Session keys must be kept secure and not shared between users",
190
+ "Files in session folders are private and only accessible with the session key",
191
+ "Public folder is shared across all users",
192
+ "Sessions expire after 24 hours of inactivity"
193
+ ]
194
+ },
195
+ "documentTypes": {
196
+ "supported": [
197
+ {
198
+ "type": "DOCX",
199
+ "description": "Microsoft Word documents",
200
+ "capabilities": ["generate", "read", "analyze"]
201
+ },
202
+ {
203
+ "type": "PDF",
204
+ "description": "Portable Document Format",
205
+ "capabilities": ["generate", "read_metadata"]
206
+ },
207
+ {
208
+ "type": "PowerPoint",
209
+ "description": "Presentation slides",
210
+ "capabilities": ["generate", "read_metadata"]
211
+ },
212
+ {
213
+ "type": "Excel",
214
+ "description": "Spreadsheets with formulas",
215
+ "capabilities": ["generate", "read", "analyze"]
216
+ },
217
+ {
218
+ "type": "LaTeX",
219
+ "description": "LaTeX to PDF conversion",
220
+ "capabilities": ["generate"]
221
+ }
222
+ ]
223
+ },
224
+ "usage": {
225
+ "workflow": [
226
+ "1. Create a session to get a unique session key",
227
+ "2. Use the session key in x-session-key header for all operations",
228
+ "3. Generate or upload documents to your session",
229
+ "4. Process and analyze documents as needed",
230
+ "5. Download generated documents",
231
+ "6. Share files publicly if needed"
232
+ ]
233
+ }
234
+ }
mcp-server.js ADDED
@@ -0,0 +1,398 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env node
2
+
3
+ import { Server } from '@modelcontextprotocol/sdk/server/index.js';
4
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
5
+ import {
6
+ CallToolRequestSchema,
7
+ ListToolsRequestSchema,
8
+ } from '@modelcontextprotocol/sdk/types.js';
9
+ import fetch from 'node-fetch';
10
+ import { FormData, Blob } from 'node-fetch';
11
+
12
+ const BASE_URL = process.env.REUBENOS_URL || 'http://localhost:3000';
13
+
14
+ class ReubenOSMCPServer {
15
+ constructor() {
16
+ this.server = new Server(
17
+ {
18
+ name: 'reubenos-file-manager',
19
+ version: '1.0.0',
20
+ },
21
+ {
22
+ capabilities: {
23
+ tools: {},
24
+ },
25
+ }
26
+ );
27
+
28
+ this.setupToolHandlers();
29
+
30
+ // Error handling
31
+ this.server.onerror = (error) => console.error('[MCP Error]', error);
32
+ process.on('SIGINT', async () => {
33
+ await this.server.close();
34
+ process.exit(0);
35
+ });
36
+ }
37
+
38
+ setupToolHandlers() {
39
+ this.server.setRequestHandler(ListToolsRequestSchema, async () => ({
40
+ tools: [
41
+ {
42
+ name: 'create_session',
43
+ description: 'Create a new isolated session with a unique key',
44
+ inputSchema: {
45
+ type: 'object',
46
+ properties: {
47
+ metadata: {
48
+ type: 'object',
49
+ description: 'Optional metadata for the session',
50
+ },
51
+ },
52
+ },
53
+ },
54
+ {
55
+ name: 'upload_file',
56
+ description: 'Upload a file to session or public folder',
57
+ inputSchema: {
58
+ type: 'object',
59
+ properties: {
60
+ sessionKey: {
61
+ type: 'string',
62
+ description: 'Session key for authentication',
63
+ },
64
+ fileName: {
65
+ type: 'string',
66
+ description: 'Name of the file',
67
+ },
68
+ content: {
69
+ type: 'string',
70
+ description: 'File content (base64 encoded for binary files)',
71
+ },
72
+ isPublic: {
73
+ type: 'boolean',
74
+ description: 'Save to public folder',
75
+ default: false,
76
+ },
77
+ },
78
+ required: ['sessionKey', 'fileName', 'content'],
79
+ },
80
+ },
81
+ {
82
+ name: 'download_file',
83
+ description: 'Download a file from session or public folder',
84
+ inputSchema: {
85
+ type: 'object',
86
+ properties: {
87
+ sessionKey: {
88
+ type: 'string',
89
+ description: 'Session key (not required for public files)',
90
+ },
91
+ fileName: {
92
+ type: 'string',
93
+ description: 'Name of the file to download',
94
+ },
95
+ isPublic: {
96
+ type: 'boolean',
97
+ description: 'Download from public folder',
98
+ default: false,
99
+ },
100
+ },
101
+ required: ['fileName'],
102
+ },
103
+ },
104
+ {
105
+ name: 'list_files',
106
+ description: 'List files in session or public folder',
107
+ inputSchema: {
108
+ type: 'object',
109
+ properties: {
110
+ sessionKey: {
111
+ type: 'string',
112
+ description: 'Session key (not required for public files)',
113
+ },
114
+ isPublic: {
115
+ type: 'boolean',
116
+ description: 'List public files',
117
+ default: false,
118
+ },
119
+ },
120
+ },
121
+ },
122
+ {
123
+ name: 'generate_document',
124
+ description: 'Generate DOCX, PDF, PowerPoint, or Excel documents',
125
+ inputSchema: {
126
+ type: 'object',
127
+ properties: {
128
+ sessionKey: {
129
+ type: 'string',
130
+ description: 'Session key for authentication',
131
+ },
132
+ type: {
133
+ type: 'string',
134
+ enum: ['docx', 'pdf', 'ppt', 'excel', 'latex'],
135
+ description: 'Document type to generate',
136
+ },
137
+ fileName: {
138
+ type: 'string',
139
+ description: 'Output file name',
140
+ },
141
+ content: {
142
+ type: 'object',
143
+ description: 'Document content (structure varies by type)',
144
+ },
145
+ isPublic: {
146
+ type: 'boolean',
147
+ description: 'Save to public folder',
148
+ default: false,
149
+ },
150
+ },
151
+ required: ['sessionKey', 'type', 'fileName', 'content'],
152
+ },
153
+ },
154
+ {
155
+ name: 'process_document',
156
+ description: 'Read and analyze documents (DOCX, Excel, PDF, etc.)',
157
+ inputSchema: {
158
+ type: 'object',
159
+ properties: {
160
+ sessionKey: {
161
+ type: 'string',
162
+ description: 'Session key for authentication',
163
+ },
164
+ fileName: {
165
+ type: 'string',
166
+ description: 'File name to process',
167
+ },
168
+ isPublic: {
169
+ type: 'boolean',
170
+ description: 'File is in public folder',
171
+ default: false,
172
+ },
173
+ operation: {
174
+ type: 'string',
175
+ enum: ['read', 'analyze'],
176
+ description: 'Operation to perform',
177
+ default: 'read',
178
+ },
179
+ },
180
+ required: ['sessionKey', 'fileName'],
181
+ },
182
+ },
183
+ ],
184
+ }));
185
+
186
+ this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
187
+ const { name, arguments: args } = request.params;
188
+
189
+ try {
190
+ switch (name) {
191
+ case 'create_session':
192
+ return await this.createSession(args);
193
+ case 'upload_file':
194
+ return await this.uploadFile(args);
195
+ case 'download_file':
196
+ return await this.downloadFile(args);
197
+ case 'list_files':
198
+ return await this.listFiles(args);
199
+ case 'generate_document':
200
+ return await this.generateDocument(args);
201
+ case 'process_document':
202
+ return await this.processDocument(args);
203
+ default:
204
+ throw new Error(`Unknown tool: ${name}`);
205
+ }
206
+ } catch (error) {
207
+ return {
208
+ content: [
209
+ {
210
+ type: 'text',
211
+ text: `Error: ${error.message}`,
212
+ },
213
+ ],
214
+ };
215
+ }
216
+ });
217
+ }
218
+
219
+ async createSession(args) {
220
+ const response = await fetch(`${BASE_URL}/api/sessions/create`, {
221
+ method: 'POST',
222
+ headers: { 'Content-Type': 'application/json' },
223
+ body: JSON.stringify({ metadata: args.metadata || {} }),
224
+ });
225
+
226
+ const data = await response.json();
227
+ return {
228
+ content: [
229
+ {
230
+ type: 'text',
231
+ text: data.success
232
+ ? `Session created successfully!\nSession ID: ${data.session.id}\nSession Key: ${data.session.key}\n\nIMPORTANT: Save this session key securely. You'll need it for all file operations.`
233
+ : `Failed to create session: ${data.error}`,
234
+ },
235
+ ],
236
+ };
237
+ }
238
+
239
+ async uploadFile(args) {
240
+ const formData = new FormData();
241
+ const buffer = Buffer.from(args.content, args.content.includes('base64,') ? 'base64' : 'utf8');
242
+ formData.append('file', new Blob([buffer]), args.fileName);
243
+ formData.append('public', args.isPublic ? 'true' : 'false');
244
+
245
+ // Use public endpoint if public, sessions endpoint if private
246
+ const endpoint = args.isPublic
247
+ ? `${BASE_URL}/api/public/upload`
248
+ : `${BASE_URL}/api/sessions/upload`;
249
+
250
+ const headers = args.isPublic
251
+ ? {}
252
+ : { 'x-session-key': args.sessionKey };
253
+
254
+ const response = await fetch(endpoint, {
255
+ method: 'POST',
256
+ headers,
257
+ body: formData,
258
+ });
259
+
260
+ const data = await response.json();
261
+ return {
262
+ content: [
263
+ {
264
+ type: 'text',
265
+ text: data.success
266
+ ? `File uploaded successfully!\nFile: ${data.fileName}\nSize: ${data.size} bytes\nLocation: ${data.isPublic ? 'Public (data/public/)' : 'Private Session (data/files/)'}\n${args.isPublic ? 'Note: This file is accessible to everyone!' : 'Note: This file is private and only accessible with your session key.'}`
267
+ : `Failed to upload file: ${data.error}`,
268
+ },
269
+ ],
270
+ };
271
+ }
272
+
273
+ async downloadFile(args) {
274
+ const url = `${BASE_URL}/api/sessions/download?file=${encodeURIComponent(args.fileName)}${
275
+ args.isPublic ? '&public=true' : ''
276
+ }`;
277
+ const headers = args.isPublic ? {} : { 'x-session-key': args.sessionKey };
278
+
279
+ const response = await fetch(url, { headers });
280
+
281
+ if (response.ok) {
282
+ const buffer = await response.buffer();
283
+ const base64 = buffer.toString('base64');
284
+ return {
285
+ content: [
286
+ {
287
+ type: 'text',
288
+ text: `File downloaded successfully!\nFile: ${args.fileName}\nSize: ${buffer.length} bytes\nContent (base64): ${base64.substring(0, 100)}...`,
289
+ },
290
+ ],
291
+ };
292
+ } else {
293
+ return {
294
+ content: [
295
+ {
296
+ type: 'text',
297
+ text: `Failed to download file: ${response.statusText}`,
298
+ },
299
+ ],
300
+ };
301
+ }
302
+ }
303
+
304
+ async listFiles(args) {
305
+ const url = `${BASE_URL}/api/sessions/files${args.isPublic ? '?public=true' : ''}`;
306
+ const headers = args.isPublic ? {} : { 'x-session-key': args.sessionKey };
307
+
308
+ const response = await fetch(url, { headers });
309
+ const data = await response.json();
310
+
311
+ if (data.success) {
312
+ const fileList = data.files
313
+ .map((f) => `- ${f.name} (${(f.size / 1024).toFixed(2)} KB)`)
314
+ .join('\n');
315
+ return {
316
+ content: [
317
+ {
318
+ type: 'text',
319
+ text: `Files in ${data.type} folder (${data.count} files):\n${fileList || 'No files found'}`,
320
+ },
321
+ ],
322
+ };
323
+ } else {
324
+ return {
325
+ content: [
326
+ {
327
+ type: 'text',
328
+ text: `Failed to list files: ${data.error}`,
329
+ },
330
+ ],
331
+ };
332
+ }
333
+ }
334
+
335
+ async generateDocument(args) {
336
+ const response = await fetch(`${BASE_URL}/api/documents/generate`, {
337
+ method: 'POST',
338
+ headers: {
339
+ 'Content-Type': 'application/json',
340
+ 'x-session-key': args.sessionKey,
341
+ },
342
+ body: JSON.stringify({
343
+ type: args.type,
344
+ fileName: args.fileName,
345
+ content: args.content,
346
+ isPublic: args.isPublic || false,
347
+ }),
348
+ });
349
+
350
+ const data = await response.json();
351
+ return {
352
+ content: [
353
+ {
354
+ type: 'text',
355
+ text: data.success
356
+ ? `Document generated successfully!\nType: ${data.type.toUpperCase()}\nFile: ${data.fileName}\nSize: ${data.size} bytes\nLocation: ${data.isPublic ? 'Public' : 'Session'} folder`
357
+ : `Failed to generate document: ${data.error}`,
358
+ },
359
+ ],
360
+ };
361
+ }
362
+
363
+ async processDocument(args) {
364
+ const response = await fetch(`${BASE_URL}/api/documents/process`, {
365
+ method: 'POST',
366
+ headers: {
367
+ 'Content-Type': 'application/json',
368
+ 'x-session-key': args.sessionKey,
369
+ },
370
+ body: JSON.stringify({
371
+ fileName: args.fileName,
372
+ isPublic: args.isPublic || false,
373
+ operation: args.operation || 'read',
374
+ }),
375
+ });
376
+
377
+ const data = await response.json();
378
+ return {
379
+ content: [
380
+ {
381
+ type: 'text',
382
+ text: data.success
383
+ ? `Document processed:\nFile: ${data.fileName}\nType: ${data.content.type}\nContent: ${JSON.stringify(data.content, null, 2)}`
384
+ : `Failed to process document: ${data.error}`,
385
+ },
386
+ ],
387
+ };
388
+ }
389
+
390
+ async run() {
391
+ const transport = new StdioServerTransport();
392
+ await this.server.connect(transport);
393
+ console.error('ReubenOS MCP Server running...');
394
+ }
395
+ }
396
+
397
+ const server = new ReubenOSMCPServer();
398
+ server.run();
package-lock.json CHANGED
The diff for this file is too large to render. See raw diff
 
package-mcp.json ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "reubenos-mcp-server",
3
+ "version": "1.0.0",
4
+ "description": "MCP server for ReubenOS file management system",
5
+ "type": "module",
6
+ "main": "mcp-server.js",
7
+ "scripts": {
8
+ "start": "node mcp-server.js"
9
+ },
10
+ "dependencies": {
11
+ "@modelcontextprotocol/sdk": "^0.5.0",
12
+ "node-fetch": "^3.3.2"
13
+ },
14
+ "engines": {
15
+ "node": ">=18"
16
+ }
17
+ }
package.json CHANGED
@@ -2,34 +2,48 @@
2
  "name": "mpc-hackathon",
3
  "version": "0.1.0",
4
  "private": true,
 
5
  "scripts": {
6
  "dev": "next dev",
7
  "build": "next build",
8
  "start": "next start",
9
- "lint": "eslint"
 
 
10
  },
11
  "dependencies": {
12
  "@github/spark": "^0.41.23",
 
13
  "@monaco-editor/react": "^4.7.0",
14
  "@phosphor-icons/react": "^2.1.10",
15
  "@radix-ui/colors": "^3.0.0",
 
 
16
  "framer-motion": "^12.23.24",
 
 
17
  "lucide-react": "^0.553.0",
18
  "mammoth": "^1.11.0",
19
  "monaco-editor": "^0.54.0",
20
  "next": "16.0.1",
 
 
 
 
21
  "react": "19.2.0",
22
  "react-dom": "19.2.0",
23
  "react-draggable": "^4.5.0",
24
  "react-error-boundary": "^6.0.0",
25
  "react-pdf": "^10.2.0",
26
  "react-rnd": "^10.5.2",
 
27
  "tw-animate-css": "^1.4.0",
28
  "xlsx": "^0.18.5"
29
  },
30
  "devDependencies": {
31
  "@tailwindcss/postcss": "^4",
32
  "@types/node": "^20",
 
33
  "@types/react": "^19",
34
  "@types/react-dom": "^19",
35
  "eslint": "^9",
 
2
  "name": "mpc-hackathon",
3
  "version": "0.1.0",
4
  "private": true,
5
+ "type": "module",
6
  "scripts": {
7
  "dev": "next dev",
8
  "build": "next build",
9
  "start": "next start",
10
+ "lint": "eslint",
11
+ "mcp": "node mcp-server.js",
12
+ "test-mcp": "node mcp-server.js test"
13
  },
14
  "dependencies": {
15
  "@github/spark": "^0.41.23",
16
+ "@modelcontextprotocol/sdk": "^1.22.0",
17
  "@monaco-editor/react": "^4.7.0",
18
  "@phosphor-icons/react": "^2.1.10",
19
  "@radix-ui/colors": "^3.0.0",
20
+ "docx": "^9.5.1",
21
+ "exceljs": "^4.4.0",
22
  "framer-motion": "^12.23.24",
23
+ "html-pdf-node": "^1.0.8",
24
+ "latex": "^0.0.1",
25
  "lucide-react": "^0.553.0",
26
  "mammoth": "^1.11.0",
27
  "monaco-editor": "^0.54.0",
28
  "next": "16.0.1",
29
+ "node-fetch": "^3.3.2",
30
+ "officegen": "^0.6.5",
31
+ "pdfkit": "^0.17.2",
32
+ "puppeteer-core": "^24.30.0",
33
  "react": "19.2.0",
34
  "react-dom": "19.2.0",
35
  "react-draggable": "^4.5.0",
36
  "react-error-boundary": "^6.0.0",
37
  "react-pdf": "^10.2.0",
38
  "react-rnd": "^10.5.2",
39
+ "sharp": "^0.34.5",
40
  "tw-animate-css": "^1.4.0",
41
  "xlsx": "^0.18.5"
42
  },
43
  "devDependencies": {
44
  "@tailwindcss/postcss": "^4",
45
  "@types/node": "^20",
46
+ "@types/pdfkit": "^0.17.3",
47
  "@types/react": "^19",
48
  "@types/react-dom": "^19",
49
  "eslint": "^9",
start-all.sh ADDED
@@ -0,0 +1,69 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/bin/bash
2
+
3
+ echo "🚀 Starting ReubenOS Complete System"
4
+ echo "===================================="
5
+
6
+ # Colors for output
7
+ GREEN='\033[0;32m'
8
+ BLUE='\033[0;34m'
9
+ YELLOW='\033[1;33m'
10
+ NC='\033[0m' # No Color
11
+
12
+ # Check if port 3000 is available
13
+ if lsof -Pi :3000 -sTCP:LISTEN -t >/dev/null ; then
14
+ echo -e "${YELLOW}⚠️ Port 3000 is in use. ReubenOS will use port 3002${NC}"
15
+ PORT=3002
16
+ else
17
+ PORT=3000
18
+ fi
19
+
20
+ # Start ReubenOS in background
21
+ echo -e "${BLUE}Starting ReubenOS on port $PORT...${NC}"
22
+ npm run dev &
23
+ WEBAPP_PID=$!
24
+ echo "ReubenOS PID: $WEBAPP_PID"
25
+
26
+ # Wait for ReubenOS to start
27
+ echo "Waiting for ReubenOS to start..."
28
+ sleep 5
29
+
30
+ # Test if ReubenOS is running
31
+ if curl -s http://localhost:$PORT > /dev/null; then
32
+ echo -e "${GREEN}✅ ReubenOS is running on http://localhost:$PORT${NC}"
33
+ else
34
+ echo -e "${YELLOW}⚠️ ReubenOS might still be starting...${NC}"
35
+ fi
36
+
37
+ echo ""
38
+ echo -e "${GREEN}================================${NC}"
39
+ echo -e "${GREEN}✅ ReubenOS System Ready!${NC}"
40
+ echo -e "${GREEN}================================${NC}"
41
+ echo ""
42
+ echo "📋 What's Running:"
43
+ echo " 1. ReubenOS Web App: http://localhost:$PORT"
44
+ echo " 2. Session Manager: Available on desktop"
45
+ echo ""
46
+ echo "🔌 To Connect Claude Desktop:"
47
+ echo " 1. The MCP server will be started automatically by Claude Desktop"
48
+ echo " 2. Just add this to your claude_desktop_config.json:"
49
+ echo ""
50
+ echo ' {'
51
+ echo ' "mcpServers": {'
52
+ echo ' "reubenos": {'
53
+ echo ' "command": "node",'
54
+ echo " \"args\": [\"$(pwd)/mcp-server.js\"],"
55
+ echo ' "env": {'
56
+ echo " \"REUBENOS_URL\": \"http://localhost:$PORT\""
57
+ echo ' }'
58
+ echo ' }'
59
+ echo ' }'
60
+ echo ' }'
61
+ echo ""
62
+ echo "📌 To stop everything: Press Ctrl+C"
63
+ echo ""
64
+
65
+ # Keep script running and handle cleanup
66
+ trap "echo 'Shutting down...'; kill $WEBAPP_PID 2>/dev/null; exit" INT TERM
67
+
68
+ # Wait for background process
69
+ wait $WEBAPP_PID
test-document-gen.js ADDED
@@ -0,0 +1,92 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env node
2
+
3
+ // Test complex document generation
4
+ import fetch from 'node-fetch';
5
+
6
+ const BASE_URL = 'http://localhost:3000';
7
+
8
+ async function testComplexDocument() {
9
+ console.log('🧪 Testing Complex Document Generation\n');
10
+
11
+ // 1. Create session
12
+ console.log('1️⃣ Creating session...');
13
+ const sessionResp = await fetch(`${BASE_URL}/api/sessions/create`, {
14
+ method: 'POST',
15
+ headers: { 'Content-Type': 'application/json' },
16
+ body: JSON.stringify({ metadata: { test: true } })
17
+ });
18
+ const sessionData = await sessionResp.json();
19
+ console.log(`✅ Session created: ${sessionData.session.id}`);
20
+ console.log(` Key: ${sessionData.session.key.substring(0, 20)}...\n`);
21
+
22
+ const sessionKey = sessionData.session.key;
23
+
24
+ // 2. Generate document with complex structure (like the letter)
25
+ console.log('2️⃣ Generating complex document (Letter)...');
26
+ const complexContent = {
27
+ type: 'docx',
28
+ fileName: 'test-letter',
29
+ content: {
30
+ sections: [
31
+ {
32
+ type: 'header',
33
+ content: 'John Doe\nFlat 101, Shar & Sorai Apartment\nVarde Valaulikar Road, Margao, Goa\[email protected]\n\nDate: November 14, 2025'
34
+ },
35
+ {
36
+ type: 'recipient',
37
+ content: 'To,\nThe Secretary\nShar & Sorai Apartment Building\nVarde Valaulikar Road\nMargao, Goa - 403601'
38
+ },
39
+ {
40
+ type: 'subject',
41
+ content: 'Subject: Water Leakage Issue on 1st Floor'
42
+ },
43
+ {
44
+ type: 'body',
45
+ paragraphs: [
46
+ 'Dear Sir/Madam,',
47
+ 'I hope this letter finds you in good health. I am writing to bring to your attention a serious water leakage problem on the 1st floor.',
48
+ 'The leakage has been noticed since the past few days, and it appears to be coming from the ceiling. This issue poses a risk of structural damage.',
49
+ 'I request immediate inspection and necessary repairs. Please arrange for maintenance personnel to assess the situation.',
50
+ 'Thank you for your prompt attention to this urgent matter.'
51
+ ]
52
+ },
53
+ {
54
+ type: 'closing',
55
+ content: 'Yours sincerely,\n\nJohn Doe\nFlat 101'
56
+ }
57
+ ],
58
+ document_type: 'letter'
59
+ },
60
+ isPublic: true,
61
+ sessionKey
62
+ };
63
+
64
+ const docResp = await fetch(`${BASE_URL}/api/documents/generate`, {
65
+ method: 'POST',
66
+ headers: {
67
+ 'Content-Type': 'application/json',
68
+ 'x-session-key': sessionKey
69
+ },
70
+ body: JSON.stringify(complexContent)
71
+ });
72
+
73
+ const docData = await docResp.json();
74
+ console.log(`${docData.success ? '✅' : '❌'} Document generation:`);
75
+ console.log(` File: ${docData.fileName}`);
76
+ console.log(` Size: ${docData.size} bytes`);
77
+ console.log(` Location: ${docData.isPublic ? 'Public' : 'Session'} folder\n`);
78
+
79
+ // 3. List public files
80
+ console.log('3️⃣ Listing public files...');
81
+ const filesResp = await fetch(`${BASE_URL}/api/sessions/files?public=true`);
82
+ const filesData = await filesResp.json();
83
+ console.log(`✅ Found ${filesData.count} public files:`);
84
+ filesData.files.forEach(file => {
85
+ console.log(` - ${file.name} (${(file.size / 1024).toFixed(2)} KB)`);
86
+ });
87
+
88
+ console.log('\n' + '='.repeat(50));
89
+ console.log('✅ Test completed! Check data/public/ for the generated file.');
90
+ }
91
+
92
+ testComplexDocument().catch(console.error);
test-mcp-local.js ADDED
@@ -0,0 +1,100 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env node
2
+
3
+ // Test script to verify MCP server can connect to ReubenOS
4
+ import fetch from 'node-fetch';
5
+
6
+ const BASE_URL = process.env.REUBENOS_URL || 'http://localhost:3000';
7
+
8
+ console.log('🧪 Testing ReubenOS MCP Integration\n');
9
+ console.log(`Testing against: ${BASE_URL}`);
10
+ console.log('=' .repeat(50));
11
+
12
+ async function testConnection() {
13
+ try {
14
+ // Test 1: Check if ReubenOS is running
15
+ console.log('\n1️⃣ Testing ReubenOS connection...');
16
+ try {
17
+ const response = await fetch(BASE_URL);
18
+ console.log('✅ ReubenOS is running!');
19
+ } catch (error) {
20
+ console.log('❌ ReubenOS is not running. Start it with: npm run dev');
21
+ return false;
22
+ }
23
+
24
+ // Test 2: Create a session
25
+ console.log('\n2️⃣ Testing session creation...');
26
+ const createResponse = await fetch(`${BASE_URL}/api/sessions/create`, {
27
+ method: 'POST',
28
+ headers: { 'Content-Type': 'application/json' },
29
+ body: JSON.stringify({ metadata: { test: true } })
30
+ });
31
+
32
+ const sessionData = await createResponse.json();
33
+ if (sessionData.success) {
34
+ console.log('✅ Session created successfully!');
35
+ console.log(` Session ID: ${sessionData.session.id}`);
36
+ console.log(` Session Key: ${sessionData.session.key.substring(0, 20)}...`);
37
+
38
+ // Test 3: List files
39
+ console.log('\n3️⃣ Testing file listing...');
40
+ const filesResponse = await fetch(`${BASE_URL}/api/sessions/files`, {
41
+ headers: { 'x-session-key': sessionData.session.key }
42
+ });
43
+
44
+ const filesData = await filesResponse.json();
45
+ if (filesData.success) {
46
+ console.log('✅ File listing works!');
47
+ console.log(` Files in session: ${filesData.count}`);
48
+ }
49
+
50
+ // Test 4: Generate a document
51
+ console.log('\n4️⃣ Testing document generation...');
52
+ const docResponse = await fetch(`${BASE_URL}/api/documents/generate`, {
53
+ method: 'POST',
54
+ headers: {
55
+ 'Content-Type': 'application/json',
56
+ 'x-session-key': sessionData.session.key
57
+ },
58
+ body: JSON.stringify({
59
+ type: 'docx',
60
+ fileName: 'test-document',
61
+ content: {
62
+ title: 'Test Document',
63
+ content: 'This is a test document generated via MCP.'
64
+ }
65
+ })
66
+ });
67
+
68
+ const docData = await docResponse.json();
69
+ if (docData.success) {
70
+ console.log('✅ Document generation works!');
71
+ console.log(` Generated: ${docData.fileName}`);
72
+ }
73
+
74
+ return sessionData.session.key;
75
+ } else {
76
+ console.log('❌ Failed to create session:', sessionData.error);
77
+ return false;
78
+ }
79
+ } catch (error) {
80
+ console.log('❌ Error during testing:', error.message);
81
+ return false;
82
+ }
83
+ }
84
+
85
+ // Run tests
86
+ testConnection().then(sessionKey => {
87
+ console.log('\n' + '=' .repeat(50));
88
+ if (sessionKey) {
89
+ console.log('✅ All tests passed! MCP integration is working.');
90
+ console.log('\n📋 Next Steps:');
91
+ console.log('1. Add the configuration to Claude Desktop');
92
+ console.log('2. Restart Claude Desktop');
93
+ console.log('3. Test with: "Using reubenos, create a new session"');
94
+ console.log('\n🔑 Test Session Key (for manual testing):');
95
+ console.log(sessionKey);
96
+ } else {
97
+ console.log('❌ Some tests failed. Please check the errors above.');
98
+ }
99
+ console.log('=' .repeat(50));
100
+ });