botserver/ui/suite/drive/index.html
Rodrigo Rodriguez (Pragmatismo) 2dca1664dd run
- Database migrations run automatically on startup
- New QUICK_START.md with usage examples and troubleshooting
- Better handling of already-running services
2025-11-28 15:06:30 -03:00

710 lines
22 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Drive - General Bots</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
background: var(--bg-primary, #0f172a);
color: var(--text-primary, #f1f5f9);
height: 100vh;
overflow: hidden;
}
.drive-container {
display: flex;
flex-direction: column;
height: 100vh;
padding: 20px;
}
.drive-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 20px;
background: var(--bg-secondary, #1e293b);
border-radius: 12px;
margin-bottom: 20px;
}
.breadcrumb {
display: flex;
align-items: center;
gap: 8px;
font-size: 14px;
color: var(--text-secondary, #94a3b8);
}
.breadcrumb-item {
cursor: pointer;
transition: color 0.2s;
}
.breadcrumb-item:hover {
color: var(--accent-color, #3b82f6);
}
.breadcrumb-separator {
color: var(--text-tertiary, #475569);
}
.actions {
display: flex;
gap: 12px;
}
.btn {
padding: 10px 20px;
border: none;
border-radius: 8px;
cursor: pointer;
font-size: 14px;
font-weight: 500;
transition: all 0.2s;
display: flex;
align-items: center;
gap: 8px;
}
.btn-primary {
background: var(--accent-color, #3b82f6);
color: white;
}
.btn-primary:hover {
background: var(--accent-hover, #2563eb);
transform: translateY(-1px);
}
.btn-secondary {
background: var(--bg-tertiary, #334155);
color: var(--text-primary, #f1f5f9);
}
.btn-secondary:hover {
background: var(--bg-quaternary, #475569);
}
.drive-content {
flex: 1;
background: var(--bg-secondary, #1e293b);
border-radius: 12px;
padding: 20px;
overflow-y: auto;
position: relative;
}
.file-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
gap: 16px;
padding: 10px;
}
.file-item {
background: var(--bg-tertiary, #334155);
border-radius: 8px;
padding: 16px;
cursor: pointer;
transition: all 0.2s;
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
position: relative;
}
.file-item:hover {
background: var(--bg-quaternary, #475569);
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
}
.file-icon {
font-size: 48px;
margin-bottom: 12px;
}
.file-name {
font-size: 14px;
word-break: break-word;
color: var(--text-primary, #f1f5f9);
}
.file-info {
font-size: 12px;
color: var(--text-secondary, #94a3b8);
margin-top: 4px;
}
.file-actions {
position: absolute;
top: 8px;
right: 8px;
display: none;
background: var(--bg-primary, #0f172a);
border-radius: 4px;
padding: 4px;
}
.file-item:hover .file-actions {
display: flex;
gap: 4px;
}
.action-btn {
background: transparent;
border: none;
color: var(--text-secondary, #94a3b8);
cursor: pointer;
padding: 4px 8px;
border-radius: 4px;
font-size: 12px;
}
.action-btn:hover {
background: var(--accent-color, #3b82f6);
color: white;
}
.loading {
display: flex;
justify-content: center;
align-items: center;
height: 200px;
color: var(--text-secondary, #94a3b8);
}
.spinner {
border: 3px solid var(--bg-tertiary, #334155);
border-top: 3px solid var(--accent-color, #3b82f6);
border-radius: 50%;
width: 40px;
height: 40px;
animation: spin 1s linear infinite;
margin-right: 12px;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100%;
color: var(--text-secondary, #94a3b8);
}
.empty-icon {
font-size: 64px;
margin-bottom: 16px;
opacity: 0.5;
}
.modal {
display: none;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.7);
z-index: 1000;
align-items: center;
justify-content: center;
}
.modal.active {
display: flex;
}
.modal-content {
background: var(--bg-secondary, #1e293b);
border-radius: 12px;
padding: 24px;
max-width: 500px;
width: 90%;
}
.modal-header {
font-size: 18px;
font-weight: 600;
margin-bottom: 20px;
}
.form-group {
margin-bottom: 16px;
}
.form-label {
display: block;
margin-bottom: 8px;
font-size: 14px;
color: var(--text-secondary, #94a3b8);
}
.form-input {
width: 100%;
padding: 10px;
border: 1px solid var(--bg-tertiary, #334155);
border-radius: 8px;
background: var(--bg-primary, #0f172a);
color: var(--text-primary, #f1f5f9);
font-size: 14px;
}
.form-input:focus {
outline: none;
border-color: var(--accent-color, #3b82f6);
}
.modal-actions {
display: flex;
gap: 12px;
justify-content: flex-end;
margin-top: 20px;
}
.upload-area {
border: 2px dashed var(--bg-tertiary, #334155);
border-radius: 8px;
padding: 40px;
text-align: center;
cursor: pointer;
transition: all 0.2s;
margin-bottom: 16px;
}
.upload-area:hover {
border-color: var(--accent-color, #3b82f6);
background: var(--bg-tertiary, #334155);
}
.upload-area.dragover {
border-color: var(--accent-color, #3b82f6);
background: var(--bg-tertiary, #334155);
}
.notification {
position: fixed;
top: 20px;
right: 20px;
background: var(--bg-secondary, #1e293b);
padding: 16px 24px;
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
display: none;
z-index: 2000;
}
.notification.show {
display: block;
animation: slideIn 0.3s ease-out;
}
@keyframes slideIn {
from {
transform: translateX(400px);
opacity: 0;
}
to {
transform: translateX(0);
opacity: 1;
}
}
.notification.success {
border-left: 4px solid #10b981;
}
.notification.error {
border-left: 4px solid #ef4444;
}
</style>
</head>
<body>
<div class="drive-container">
<div class="drive-header">
<div class="breadcrumb">
<span class="breadcrumb-item" data-path="/">📁 Drive</span>
<span class="breadcrumb-separator">/</span>
<span class="breadcrumb-item current"></span>
</div>
<div class="actions">
<button class="btn btn-secondary" onclick="createFolder()">
📁 New Folder
</button>
<button class="btn btn-primary" onclick="uploadFile()">
⬆️ Upload
</button>
</div>
</div>
<div class="drive-content">
<div class="loading" id="loading">
<div class="spinner"></div>
Loading files...
</div>
<div class="file-grid" id="fileGrid" style="display: none;"></div>
<div class="empty-state" id="emptyState" style="display: none;">
<div class="empty-icon">📂</div>
<p>This folder is empty</p>
</div>
</div>
</div>
<!-- Upload Modal -->
<div class="modal" id="uploadModal">
<div class="modal-content">
<div class="modal-header">Upload File</div>
<div class="upload-area" id="uploadArea" onclick="document.getElementById('fileInput').click()">
<p>📤 Click to select or drag files here</p>
<input type="file" id="fileInput" style</p>="display: none;" multiple>
</div>
<div id="uploadProgress"></div>
<div class="modal-actions">
<button class="btn btn-secondary" onclick="closeUploadModal()">Cancel</button>
</div>
</div>
</div>
<!-- Create Folder Modal -->
<div class="modal" id="folderModal">
<div class="modal-content">
<div class="modal-header">Create New Folder</div>
<div class="form-group">
<label class="form-label">Folder Name</label>
<input type="text" class="form-input" id="folderName" placeholder="Enter folder name">
</div>
<div class="modal-actions">
<button class="btn btn-secondary" onclick="closeFolderModal()">Cancel</button>
<button class="btn btn-primary" onclick="submitCreateFolder()">Create</button>
</div>
</div>
</div>
<!-- Notification -->
<div class="notification" id="notification"></div>
<script>
const API_BASE = window.location.origin;
let currentPath = '/';
// Load files on page load
document.addEventListener('DOMContentLoaded', () => {
loadFiles(currentPath);
setupDragAndDrop();
});
// Load files from API
async function loadFiles(path) {
const loading = document.getElementById('loading');
const fileGrid = document.getElementById('fileGrid');
const emptyState = document.getElementById('emptyState');
loading.style.display = 'flex';
fileGrid.style.display = 'none';
emptyState.style.display = 'none';
try {
const response = await fetch(`${API_BASE}/api/drive/list?path=${encodeURIComponent(path)}`);
const data = await response.json();
const files = Array.isArray(data) ? data : [];
loading.style.display = 'none';
if (files.length === 0) {
emptyState.style.display = 'flex';
} else {
fileGrid.style.display = 'grid';
renderFiles(files);
}
updateBreadcrumb(path);
} catch (error) {
console.error('Failed to load files:', error);
showNotification('Failed to load files', 'error');
loading.style.display = 'none';
emptyState.style.display = 'flex';
}
}
// Render files in grid
function renderFiles(files) {
const fileGrid = document.getElementById('fileGrid');
fileGrid.innerHTML = '';
files.forEach(file => {
const fileItem = document.createElement('div');
fileItem.className = 'file-item';
fileItem.onclick = () => handleFileClick(file);
const icon = file.is_dir ? '📁' : getFileIcon(file.name);
const size = file.is_dir ? '' : formatBytes(file.size);
fileItem.innerHTML = `
<div class="file-icon">${icon}</div>
<div class="file-name">${file.name}</div>
<div class="file-info">${size}</div>
<div class="file-actions">
<button class="action-btn" onclick="event.stopPropagation(); downloadFile('${file.path}')">⬇️</button>
<button class="action-btn" onclick="event.stopPropagation(); deleteFile('${file.path}')">🗑️</button>
</div>
`;
fileGrid.appendChild(fileItem);
});
}
// Handle file/folder click
function handleFileClick(file) {
if (file.is_dir) {
currentPath = file.path;
loadFiles(currentPath);
} else {
downloadFile(file.path);
}
}
// Get file icon based on extension
function getFileIcon(filename) {
const ext = filename.split('.').pop().toLowerCase();
const icons = {
'pdf': '📄',
'doc': '📝', 'docx': '📝',
'xls': '📊', 'xlsx': '📊',
'jpg': '🖼️', 'jpeg': '🖼️', 'png': '🖼️', 'gif': '🖼️',
'mp4': '🎬', 'avi': '🎬', 'mov': '🎬',
'mp3': '🎵', 'wav': '🎵',
'zip': '📦', 'rar': '📦', 'tar': '📦',
'txt': '📃',
'js': '💻', 'html': '💻', 'css': '💻', 'py': '💻',
};
return icons[ext] || '📄';
}
// Format bytes to human readable
function formatBytes(bytes) {
if (bytes === 0) return '0 B';
const k = 1024;
const sizes = ['B', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return Math.round(bytes / Math.pow(k, i) * 100) / 100 + ' ' + sizes[i];
}
// Update breadcrumb
function updateBreadcrumb(path) {
const breadcrumb = document.querySelector('.breadcrumb');
const parts = path.split('/').filter(p => p);
breadcrumb.innerHTML = '<span class="breadcrumb-item" onclick="navigateTo(\'/\')">📁 Drive</span>';
let accumulated = '';
parts.forEach((part, index) => {
accumulated += '/' + part;
const isLast = index === parts.length - 1;
breadcrumb.innerHTML += ` <span class="breadcrumb-separator">/</span> `;
if (isLast) {
breadcrumb.innerHTML += `<span class="breadcrumb-item current">${part}</span>`;
} else {
const pathCopy = accumulated;
breadcrumb.innerHTML += `<span class="breadcrumb-item" onclick="navigateTo('${pathCopy}')">${part}</span>`;
}
});
}
// Navigate to path
function navigateTo(path) {
currentPath = path;
loadFiles(path);
}
// Upload file modal
function uploadFile() {
document.getElementById('uploadModal').classList.add('active');
}
function closeUploadModal() {
document.getElementById('uploadModal').classList.remove('active');
document.getElementById('fileInput').value = '';
}
// Handle file selection
document.getElementById('fileInput').addEventListener('change', async (e) => {
const files = e.target.files;
if (files.length > 0) {
await uploadFiles(files);
}
});
// Upload files to API
async function uploadFiles(files) {
const progress = document.getElementById('uploadProgress');
for (let i = 0; i < files.length; i++) {
const file = files[i];
progress.innerHTML = `Uploading ${file.name}... (${i + 1}/${files.length})`;
const formData = new FormData();
formData.append('file', file);
formData.append('path', currentPath);
try {
const response = await fetch(`${API_BASE}/api/drive/upload`, {
method: 'POST',
body: formData
});
if (response.ok) {
showNotification(`Uploaded ${file.name}`, 'success');
} else {
showNotification(`Failed to upload ${file.name}`, 'error');
}
} catch (error) {
console.error('Upload error:', error);
showNotification(`Error uploading ${file.name}`, 'error');
}
}
closeUploadModal();
loadFiles(currentPath);
}
// Drag and drop
function setupDragAndDrop() {
const uploadArea = document.getElementById('uploadArea');
['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => {
uploadArea.addEventListener(eventName, preventDefaults, false);
});
function preventDefaults(e) {
e.preventDefault();
e.stopPropagation();
}
['dragenter', 'dragover'].forEach(eventName => {
uploadArea.addEventListener(eventName, () => {
uploadArea.classList.add('dragover');
}, false);
});
['dragleave', 'drop'].forEach(eventName => {
uploadArea.addEventListener(eventName, () => {
uploadArea.classList.remove('dragover');
}, false);
});
uploadArea.addEventListener('drop', (e) => {
const files = e.dataTransfer.files;
if (files.length > 0) {
uploadFiles(files);
}
}, false);
}
// Create folder modal
function createFolder() {
document.getElementById('folderModal').classList.add('active');
document.getElementById('folderName').value = '';
}
function closeFolderModal() {
document.getElementById('folderModal').classList.remove('active');
}
async function submitCreateFolder() {
const folderName = document.getElementById('folderName').value.trim();
if (!folderName) {
showNotification('Please enter a folder name', 'error');
return;
}
try {
const response = await fetch(`${API_BASE}/api/drive/folder`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
path: currentPath,
name: folderName
})
});
if (response.ok) {
showNotification('Folder created successfully', 'success');
closeFolderModal();
loadFiles(currentPath);
} else {
showNotification('Failed to create folder', 'error');
}
} catch (error) {
console.error('Create folder error:', error);
showNotification('Error creating folder', 'error');
}
}
// Download file
async function downloadFile(path) {
window.open(`${API_BASE}/api/drive/download${path}`, '_blank');
}
// Delete file
async function deleteFile(path) {
if (!confirm('Are you sure you want to delete this item?')) {
return;
}
try {
const response = await fetch(`${API_BASE}/api/drive/file`, {
method: 'DELETE',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ path })
});
if (response.ok) {
showNotification('Deleted successfully', 'success');
loadFiles(currentPath);
} else {
showNotification('Failed to delete', 'error');
}
} catch (error) {
console.error('Delete error:', error);
showNotification('Error deleting item', 'error');
}
}
// Show notification
function showNotification(message, type = 'success') {
const notification = document.getElementById('notification');
notification.textContent = message;
notification.className = `notification ${type} show`;
setTimeout(() => {
notification.classList.remove('show');
}, 3000);
}
// Keyboard shortcuts
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape') {
closeUploadModal();
closeFolderModal();
}
});
</script>
</body>
</html>