Spaces:
Running
Running
fix: File preview for secure data and open .dart files in Flutter IDE with content
Browse files- app/components/Desktop.tsx +11 -3
- app/components/FileManager.tsx +22 -2
- app/components/FilePreview.tsx +27 -8
- test-mcp-endpoint.js +150 -0
app/components/Desktop.tsx
CHANGED
|
@@ -194,9 +194,17 @@ export function Desktop() {
|
|
| 194 |
|
| 195 |
|
| 196 |
const openFlutterCodeEditor = () => {
|
| 197 |
-
|
| 198 |
-
|
| 199 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 200 |
}
|
| 201 |
|
| 202 |
const closeFlutterCodeEditor = () => {
|
|
|
|
| 194 |
|
| 195 |
|
| 196 |
const openFlutterCodeEditor = () => {
|
| 197 |
+
// Check if there's file content stored from File Manager
|
| 198 |
+
const storedContent = sessionStorage.getItem('flutterFileContent')
|
| 199 |
+
if (storedContent) {
|
| 200 |
+
setActiveFlutterApp({ dartCode: storedContent })
|
| 201 |
+
sessionStorage.removeItem('flutterFileContent') // Clean up
|
| 202 |
+
} else {
|
| 203 |
+
setActiveFlutterApp(null) // Use default template
|
| 204 |
+
}
|
| 205 |
+
setFlutterRunnerOpen(true)
|
| 206 |
+
setFlutterRunnerMinimized(false)
|
| 207 |
+
setWindowZIndices(prev => ({ ...prev, flutterRunner: getNextZIndex() }))
|
| 208 |
}
|
| 209 |
|
| 210 |
const closeFlutterCodeEditor = () => {
|
app/components/FileManager.tsx
CHANGED
|
@@ -496,13 +496,31 @@ export function FileManager({ currentPath, onNavigate, onClose, onOpenFlutterApp
|
|
| 496 |
className="group relative"
|
| 497 |
>
|
| 498 |
<button
|
| 499 |
-
onDoubleClick={() => {
|
| 500 |
if (file.type === 'folder') {
|
| 501 |
onNavigate(file.path)
|
| 502 |
} else if (file.type === 'flutter_app' && onOpenFlutterApp) {
|
| 503 |
onOpenFlutterApp(file)
|
| 504 |
} else if (file.extension === 'dart' || file.extension === 'flutter') {
|
| 505 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 506 |
} else if (file.extension === 'tex') {
|
| 507 |
if (onOpenApp) onOpenApp('latex-editor')
|
| 508 |
} else {
|
|
@@ -636,6 +654,8 @@ export function FileManager({ currentPath, onNavigate, onClose, onOpenFlutterApp
|
|
| 636 |
file={previewFile}
|
| 637 |
onClose={() => setPreviewFile(null)}
|
| 638 |
onDownload={() => handleDownload(previewFile)}
|
|
|
|
|
|
|
| 639 |
/>
|
| 640 |
)}
|
| 641 |
</>
|
|
|
|
| 496 |
className="group relative"
|
| 497 |
>
|
| 498 |
<button
|
| 499 |
+
onDoubleClick={async () => {
|
| 500 |
if (file.type === 'folder') {
|
| 501 |
onNavigate(file.path)
|
| 502 |
} else if (file.type === 'flutter_app' && onOpenFlutterApp) {
|
| 503 |
onOpenFlutterApp(file)
|
| 504 |
} else if (file.extension === 'dart' || file.extension === 'flutter') {
|
| 505 |
+
// Load the file content and open Flutter IDE with it
|
| 506 |
+
try {
|
| 507 |
+
let fileContent = ''
|
| 508 |
+
if (sidebarSelection === 'secure' && passkey) {
|
| 509 |
+
const response = await fetch(`/api/data?key=${encodeURIComponent(passkey)}&folder=`)
|
| 510 |
+
const data = await response.json()
|
| 511 |
+
const fileData = data.files?.find((f: any) => f.name === file.name)
|
| 512 |
+
fileContent = fileData?.content || ''
|
| 513 |
+
}
|
| 514 |
+
// Open Flutter IDE with the file content
|
| 515 |
+
if (onOpenApp) {
|
| 516 |
+
// Store the content temporarily for the Flutter IDE to pick up
|
| 517 |
+
sessionStorage.setItem('flutterFileContent', fileContent)
|
| 518 |
+
onOpenApp('flutter-editor')
|
| 519 |
+
}
|
| 520 |
+
} catch (error) {
|
| 521 |
+
console.error('Error loading file:', error)
|
| 522 |
+
if (onOpenApp) onOpenApp('flutter-editor')
|
| 523 |
+
}
|
| 524 |
} else if (file.extension === 'tex') {
|
| 525 |
if (onOpenApp) onOpenApp('latex-editor')
|
| 526 |
} else {
|
|
|
|
| 654 |
file={previewFile}
|
| 655 |
onClose={() => setPreviewFile(null)}
|
| 656 |
onDownload={() => handleDownload(previewFile)}
|
| 657 |
+
passkey={passkey}
|
| 658 |
+
isPublic={sidebarSelection === 'public'}
|
| 659 |
/>
|
| 660 |
)}
|
| 661 |
</>
|
app/components/FilePreview.tsx
CHANGED
|
@@ -15,9 +15,11 @@ interface FilePreviewProps {
|
|
| 15 |
}
|
| 16 |
onClose: () => void
|
| 17 |
onDownload: () => void
|
|
|
|
|
|
|
| 18 |
}
|
| 19 |
|
| 20 |
-
export function FilePreview({ file, onClose, onDownload }: FilePreviewProps) {
|
| 21 |
const [content, setContent] = useState<any>(null)
|
| 22 |
const [loading, setLoading] = useState(true)
|
| 23 |
const [error, setError] = useState<string | null>(null)
|
|
@@ -26,7 +28,11 @@ export function FilePreview({ file, onClose, onDownload }: FilePreviewProps) {
|
|
| 26 |
// Excel specific state
|
| 27 |
const [activeSheet, setActiveSheet] = useState(0)
|
| 28 |
|
| 29 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 30 |
const ext = file.extension?.toLowerCase() || ''
|
| 31 |
|
| 32 |
useEffect(() => {
|
|
@@ -69,6 +75,8 @@ export function FilePreview({ file, onClose, onDownload }: FilePreviewProps) {
|
|
| 69 |
case 'sh':
|
| 70 |
case 'yaml':
|
| 71 |
case 'yml':
|
|
|
|
|
|
|
| 72 |
await loadTextFile()
|
| 73 |
break
|
| 74 |
case 'csv':
|
|
@@ -124,9 +132,21 @@ export function FilePreview({ file, onClose, onDownload }: FilePreviewProps) {
|
|
| 124 |
|
| 125 |
const loadTextFile = async () => {
|
| 126 |
try {
|
| 127 |
-
|
| 128 |
-
|
| 129 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 130 |
} catch (err) {
|
| 131 |
throw new Error('Failed to load text file')
|
| 132 |
}
|
|
@@ -234,9 +254,8 @@ export function FilePreview({ file, onClose, onDownload }: FilePreviewProps) {
|
|
| 234 |
<button
|
| 235 |
key={index}
|
| 236 |
onClick={() => setActiveSheet(index)}
|
| 237 |
-
className={`px-3 py-1 rounded ${
|
| 238 |
-
|
| 239 |
-
}`}
|
| 240 |
>
|
| 241 |
{sheet.name}
|
| 242 |
</button>
|
|
|
|
| 15 |
}
|
| 16 |
onClose: () => void
|
| 17 |
onDownload: () => void
|
| 18 |
+
passkey?: string
|
| 19 |
+
isPublic?: boolean
|
| 20 |
}
|
| 21 |
|
| 22 |
+
export function FilePreview({ file, onClose, onDownload, passkey, isPublic = true }: FilePreviewProps) {
|
| 23 |
const [content, setContent] = useState<any>(null)
|
| 24 |
const [loading, setLoading] = useState(true)
|
| 25 |
const [error, setError] = useState<string | null>(null)
|
|
|
|
| 28 |
// Excel specific state
|
| 29 |
const [activeSheet, setActiveSheet] = useState(0)
|
| 30 |
|
| 31 |
+
// For secure data, we read directly from /api/data
|
| 32 |
+
// For public, use the download API
|
| 33 |
+
const previewUrl = isPublic
|
| 34 |
+
? `/api/download?path=${encodeURIComponent(file.path)}&preview=true`
|
| 35 |
+
: `/api/data?key=${encodeURIComponent(passkey || '')}&path=${encodeURIComponent(file.path)}`
|
| 36 |
const ext = file.extension?.toLowerCase() || ''
|
| 37 |
|
| 38 |
useEffect(() => {
|
|
|
|
| 75 |
case 'sh':
|
| 76 |
case 'yaml':
|
| 77 |
case 'yml':
|
| 78 |
+
case 'dart':
|
| 79 |
+
case 'tex':
|
| 80 |
await loadTextFile()
|
| 81 |
break
|
| 82 |
case 'csv':
|
|
|
|
| 132 |
|
| 133 |
const loadTextFile = async () => {
|
| 134 |
try {
|
| 135 |
+
// For secure data files, read from /api/data which returns file list with content
|
| 136 |
+
if (!isPublic && passkey) {
|
| 137 |
+
const response = await fetch(`/api/data?key=${encodeURIComponent(passkey)}&folder=`)
|
| 138 |
+
const data = await response.json()
|
| 139 |
+
const fileData = data.files?.find((f: any) => f.name === file.name)
|
| 140 |
+
if (fileData && fileData.content) {
|
| 141 |
+
setContent({ type: 'text', data: fileData.content })
|
| 142 |
+
} else {
|
| 143 |
+
throw new Error('File content not available')
|
| 144 |
+
}
|
| 145 |
+
} else {
|
| 146 |
+
const response = await fetch(previewUrl)
|
| 147 |
+
const text = await response.text()
|
| 148 |
+
setContent({ type: 'text', data: text })
|
| 149 |
+
}
|
| 150 |
} catch (err) {
|
| 151 |
throw new Error('Failed to load text file')
|
| 152 |
}
|
|
|
|
| 254 |
<button
|
| 255 |
key={index}
|
| 256 |
onClick={() => setActiveSheet(index)}
|
| 257 |
+
className={`px-3 py-1 rounded ${activeSheet === index ? 'bg-blue-500 text-white' : 'bg-gray-100'
|
| 258 |
+
}`}
|
|
|
|
| 259 |
>
|
| 260 |
{sheet.name}
|
| 261 |
</button>
|
test-mcp-endpoint.js
ADDED
|
@@ -0,0 +1,150 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// Test script for MCP handler API
|
| 2 |
+
// Run with: node test-mcp-endpoint.js
|
| 3 |
+
|
| 4 |
+
const BASE_URL = 'https://mcp-1st-birthday-reuben-os.hf.space';
|
| 5 |
+
const API_URL = `${BASE_URL}/api/mcp-handler`;
|
| 6 |
+
|
| 7 |
+
async function testMCPEndpoint() {
|
| 8 |
+
console.log('π§ͺ Testing MCP Handler API on Hugging Face...\n');
|
| 9 |
+
|
| 10 |
+
// Test 1: GET without passkey (should fail with error)
|
| 11 |
+
console.log('Test 1: GET without passkey');
|
| 12 |
+
try {
|
| 13 |
+
const response = await fetch(API_URL);
|
| 14 |
+
const data = await response.json();
|
| 15 |
+
console.log('β
Response:', JSON.stringify(data, null, 2));
|
| 16 |
+
console.log('Expected error message: β\n');
|
| 17 |
+
} catch (error) {
|
| 18 |
+
console.log('β Error:', error.message, '\n');
|
| 19 |
+
}
|
| 20 |
+
|
| 21 |
+
// Test 2: POST - Save a test file to public folder
|
| 22 |
+
console.log('Test 2: POST - Save public file');
|
| 23 |
+
try {
|
| 24 |
+
const response = await fetch(API_URL, {
|
| 25 |
+
method: 'POST',
|
| 26 |
+
headers: { 'Content-Type': 'application/json' },
|
| 27 |
+
body: JSON.stringify({
|
| 28 |
+
action: 'save_file',
|
| 29 |
+
fileName: 'test.txt',
|
| 30 |
+
content: 'Hello from MCP test!',
|
| 31 |
+
isPublic: true,
|
| 32 |
+
}),
|
| 33 |
+
});
|
| 34 |
+
const data = await response.json();
|
| 35 |
+
console.log('β
Response:', JSON.stringify(data, null, 2));
|
| 36 |
+
if (data.success) {
|
| 37 |
+
console.log('β File saved successfully to public folder!\n');
|
| 38 |
+
} else {
|
| 39 |
+
console.log('β Save failed:', data.error, '\n');
|
| 40 |
+
}
|
| 41 |
+
} catch (error) {
|
| 42 |
+
console.log('β Error:', error.message, '\n');
|
| 43 |
+
}
|
| 44 |
+
|
| 45 |
+
// Test 3: GET - List public files
|
| 46 |
+
console.log('Test 3: GET - List public files');
|
| 47 |
+
try {
|
| 48 |
+
const url = new URL(API_URL);
|
| 49 |
+
url.searchParams.set('isPublic', 'true');
|
| 50 |
+
|
| 51 |
+
const response = await fetch(url);
|
| 52 |
+
const data = await response.json();
|
| 53 |
+
console.log('β
Response:', JSON.stringify(data, null, 2));
|
| 54 |
+
if (data.success) {
|
| 55 |
+
console.log(`β Found ${data.count} public file(s)\n`);
|
| 56 |
+
}
|
| 57 |
+
} catch (error) {
|
| 58 |
+
console.log('β Error:', error.message, '\n');
|
| 59 |
+
}
|
| 60 |
+
|
| 61 |
+
// Test 4: POST - Save with passkey
|
| 62 |
+
console.log('Test 4: POST - Save with passkey');
|
| 63 |
+
const testPasskey = 'test-passkey-123';
|
| 64 |
+
try {
|
| 65 |
+
const response = await fetch(API_URL, {
|
| 66 |
+
method: 'POST',
|
| 67 |
+
headers: { 'Content-Type': 'application/json' },
|
| 68 |
+
body: JSON.stringify({
|
| 69 |
+
passkey: testPasskey,
|
| 70 |
+
action: 'save_file',
|
| 71 |
+
fileName: 'secure_test.dart',
|
| 72 |
+
content: `// Flutter test file\nvoid main() {\n print('Hello from secure storage!');\n}`,
|
| 73 |
+
isPublic: false,
|
| 74 |
+
}),
|
| 75 |
+
});
|
| 76 |
+
const data = await response.json();
|
| 77 |
+
console.log('β
Response:', JSON.stringify(data, null, 2));
|
| 78 |
+
if (data.success) {
|
| 79 |
+
console.log('β File saved successfully to secure storage!\n');
|
| 80 |
+
}
|
| 81 |
+
} catch (error) {
|
| 82 |
+
console.log('β Error:', error.message, '\n');
|
| 83 |
+
}
|
| 84 |
+
|
| 85 |
+
// Test 5: GET - List files with passkey
|
| 86 |
+
console.log('Test 5: GET - List files with passkey');
|
| 87 |
+
try {
|
| 88 |
+
const url = new URL(API_URL);
|
| 89 |
+
url.searchParams.set('passkey', testPasskey);
|
| 90 |
+
|
| 91 |
+
const response = await fetch(url);
|
| 92 |
+
const data = await response.json();
|
| 93 |
+
console.log('β
Response:', JSON.stringify(data, null, 2));
|
| 94 |
+
if (data.success) {
|
| 95 |
+
console.log(`β Found ${data.count} file(s) for passkey: ${testPasskey}\n`);
|
| 96 |
+
}
|
| 97 |
+
} catch (error) {
|
| 98 |
+
console.log('β Error:', error.message, '\n');
|
| 99 |
+
}
|
| 100 |
+
|
| 101 |
+
// Test 6: Deploy a test quiz
|
| 102 |
+
console.log('Test 6: POST - Deploy quiz');
|
| 103 |
+
try {
|
| 104 |
+
const quizData = {
|
| 105 |
+
title: 'Test Quiz',
|
| 106 |
+
description: 'A test quiz from MCP',
|
| 107 |
+
questions: [
|
| 108 |
+
{
|
| 109 |
+
id: 'q1',
|
| 110 |
+
question: 'What is 2+2?',
|
| 111 |
+
type: 'multiple-choice',
|
| 112 |
+
options: ['3', '4', '5', '6'],
|
| 113 |
+
correctAnswer: 1,
|
| 114 |
+
points: 1,
|
| 115 |
+
},
|
| 116 |
+
],
|
| 117 |
+
};
|
| 118 |
+
|
| 119 |
+
const response = await fetch(API_URL, {
|
| 120 |
+
method: 'POST',
|
| 121 |
+
headers: { 'Content-Type': 'application/json' },
|
| 122 |
+
body: JSON.stringify({
|
| 123 |
+
passkey: testPasskey,
|
| 124 |
+
action: 'deploy_quiz',
|
| 125 |
+
fileName: 'quiz.json',
|
| 126 |
+
content: JSON.stringify(quizData, null, 2),
|
| 127 |
+
isPublic: false,
|
| 128 |
+
}),
|
| 129 |
+
});
|
| 130 |
+
const data = await response.json();
|
| 131 |
+
console.log('β
Response:', JSON.stringify(data, null, 2));
|
| 132 |
+
if (data.success) {
|
| 133 |
+
console.log('β Quiz deployed successfully!\n');
|
| 134 |
+
}
|
| 135 |
+
} catch (error) {
|
| 136 |
+
console.log('β Error:', error.message, '\n');
|
| 137 |
+
}
|
| 138 |
+
|
| 139 |
+
console.log('π All tests completed!\n');
|
| 140 |
+
console.log('Summary:');
|
| 141 |
+
console.log('- MCP Handler API is accessible');
|
| 142 |
+
console.log('- Public file uploads work');
|
| 143 |
+
console.log('- Passkey-protected uploads work');
|
| 144 |
+
console.log('- File listing works');
|
| 145 |
+
console.log('- Quiz deployment works');
|
| 146 |
+
console.log('\nβ
Claude Desktop can now upload files to ReubenOS!');
|
| 147 |
+
}
|
| 148 |
+
|
| 149 |
+
// Run tests
|
| 150 |
+
testMCPEndpoint().catch(console.error);
|