Spaces:
Running
Running
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 +18 -1
- CLAUDE_DESKTOP_SETUP.md +215 -58
- Empc-hackathonbackendmcp_server.py +0 -0
- Empc-hackathonpublicbackground_readme.txt +0 -1
- RUN_INSTRUCTIONS.md +134 -0
- SESSION_SYSTEM.md +230 -0
- app/api/code/execute/route.ts +0 -186
- app/api/code/public/route.ts +0 -85
- app/api/code/save/route.ts +0 -130
- app/api/documents/generate/route.ts +194 -0
- app/api/documents/process/route.ts +216 -0
- app/api/public/upload/route.ts +58 -0
- app/api/sessions/create/route.ts +40 -0
- app/api/sessions/download/route.ts +102 -0
- app/api/sessions/files/route.ts +102 -0
- app/api/sessions/upload/route.ts +87 -0
- app/components/CodeExecutor.tsx +0 -306
- app/components/CodePlayground.tsx +0 -668
- app/components/Desktop.tsx +83 -86
- app/components/Dock.tsx +2 -28
- app/components/DraggableDesktopIcon.tsx +8 -1
- app/components/SessionManager.tsx +543 -0
- app/components/SessionManagerWindow.tsx +602 -0
- app/components/VSCodeEditor.tsx +0 -458
- app/sessions/page.tsx +5 -0
- lib/documentGenerators.ts +380 -0
- lib/sessionManager.ts +207 -0
- mcp-config.json +234 -0
- mcp-server.js +398 -0
- package-lock.json +0 -0
- package-mcp.json +17 -0
- package.json +15 -1
- start-all.sh +69 -0
- test-document-gen.js +92 -0
- test-mcp-local.js +100 -0
.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 |
-
#
|
| 2 |
|
| 3 |
-
|
| 4 |
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 8 |
```
|
| 9 |
|
| 10 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 11 |
```
|
| 12 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 13 |
```
|
| 14 |
|
| 15 |
-
|
|
|
|
|
|
|
| 16 |
|
| 17 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 18 |
|
| 19 |
```json
|
| 20 |
{
|
| 21 |
"mcpServers": {
|
| 22 |
-
"
|
| 23 |
-
"command": "
|
| 24 |
-
"args": ["
|
| 25 |
"env": {
|
| 26 |
-
"
|
| 27 |
-
"DATA_DIR": "E:\\mpc-hackathon\\data"
|
| 28 |
}
|
| 29 |
}
|
| 30 |
}
|
| 31 |
}
|
| 32 |
```
|
| 33 |
|
| 34 |
-
## Step
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 35 |
|
| 36 |
-
|
| 37 |
-
2. Open Claude Desktop again
|
| 38 |
-
3. The MCP server should start automatically
|
| 39 |
|
| 40 |
-
|
| 41 |
|
| 42 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 43 |
|
| 44 |
-
|
| 45 |
-
- Claude should mention "semsoon"
|
| 46 |
|
| 47 |
-
|
| 48 |
-
|
|
|
|
|
|
|
| 49 |
|
| 50 |
-
|
| 51 |
-
|
| 52 |
|
| 53 |
-
|
| 54 |
-
|
| 55 |
|
| 56 |
-
|
|
|
|
|
|
|
| 57 |
|
| 58 |
-
###
|
| 59 |
-
Make sure Python is in your PATH:
|
| 60 |
-
```cmd
|
| 61 |
-
python --version
|
| 62 |
```
|
|
|
|
|
|
|
| 63 |
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 67 |
```
|
| 68 |
|
| 69 |
-
###
|
| 70 |
-
|
| 71 |
-
|
|
|
|
|
|
|
|
|
|
| 72 |
```
|
| 73 |
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 78 |
```
|
| 79 |
-
Should show: "Uvicorn running on http://127.0.0.1:8000"
|
| 80 |
|
| 81 |
-
|
| 82 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 83 |
|
| 84 |
-
|
|
|
|
|
|
|
|
|
|
| 85 |
|
| 86 |
-
|
| 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 |
-
|
|
|
|
|
|
|
| 94 |
|
| 95 |
-
|
| 96 |
-
✅ Sample documents have been created in E:\mpc-hackathon\data\documents
|
| 97 |
-
✅ Sample exams have been added to the database
|
| 98 |
|
| 99 |
-
|
|
|
|
| 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! 🎉
|
Empc-hackathonbackendmcp_server.py
DELETED
|
File without changes
|
Empc-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 |
+
-F "[email protected]"
|
| 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 {
|
| 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>(
|
|
|
|
|
|
|
| 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 [
|
| 47 |
|
| 48 |
const openFileManager = (path: string) => {
|
| 49 |
setCurrentPath(path)
|
|
@@ -94,28 +92,12 @@ export function Desktop() {
|
|
| 94 |
setTerminalOpen(false)
|
| 95 |
}
|
| 96 |
|
| 97 |
-
const
|
| 98 |
-
|
| 99 |
}
|
| 100 |
|
| 101 |
-
const
|
| 102 |
-
|
| 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 '
|
| 142 |
-
|
| 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}
|
| 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="
|
| 339 |
-
label="
|
| 340 |
-
iconType="
|
| 341 |
-
initialPosition={{ x:
|
| 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={
|
| 360 |
/>
|
| 361 |
</div>
|
| 362 |
|
|
@@ -418,35 +427,23 @@ export function Desktop() {
|
|
| 418 |
</motion.div>
|
| 419 |
)}
|
| 420 |
|
| 421 |
-
|
| 422 |
-
|
| 423 |
-
|
| 424 |
-
|
| 425 |
-
|
| 426 |
-
|
| 427 |
-
|
| 428 |
-
|
| 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 |
+
});
|