| """
|
| GUI Interface for Docking@HOME
|
|
|
| A modern web-based GUI using FastAPI and HTML/JavaScript for molecular docking.
|
| Integrates with AutoDock backend for real molecular docking simulations.
|
|
|
| Authors: OpenPeer AI, Riemann Computing Inc., Bleunomics, Andrew Magdy Kamal
|
| """
|
|
|
| import os
|
| import json
|
| import asyncio
|
| from pathlib import Path
|
| from typing import Optional, List, Dict
|
| from datetime import datetime
|
|
|
| from fastapi import FastAPI, File, UploadFile, HTTPException, WebSocket, WebSocketDisconnect
|
| from fastapi.responses import HTMLResponse, FileResponse, JSONResponse
|
| from fastapi.staticfiles import StaticFiles
|
| from pydantic import BaseModel
|
| import uvicorn
|
|
|
|
|
| from .server import job_manager, initialize_server
|
|
|
|
|
| app = FastAPI(
|
| title="Docking@HOME",
|
| description="Distributed Molecular Docking Platform",
|
| version="1.0.0"
|
| )
|
|
|
|
|
| class DockingJobRequest(BaseModel):
|
| num_runs: int = 100
|
| use_gpu: bool = True
|
| job_name: Optional[str] = None
|
|
|
| class JobStatus(BaseModel):
|
| job_id: str
|
| status: str
|
| progress: float
|
| message: str
|
|
|
|
|
| active_websockets: List[WebSocket] = []
|
|
|
|
|
| UPLOAD_DIR = Path("uploads")
|
| UPLOAD_DIR.mkdir(exist_ok=True)
|
|
|
| RESULTS_DIR = Path("results")
|
| RESULTS_DIR.mkdir(exist_ok=True)
|
|
|
|
|
| @app.get("/", response_class=HTMLResponse)
|
| async def root():
|
| """Serve the main GUI page"""
|
| html_content = """
|
| <!DOCTYPE html>
|
| <html lang="en">
|
| <head>
|
| <meta charset="UTF-8">
|
| <meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| <title>Docking@HOME - Molecular Docking Platform</title>
|
| <style>
|
| * {
|
| margin: 0;
|
| padding: 0;
|
| box-sizing: border-box;
|
| }
|
|
|
| body {
|
| font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
| background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
| min-height: 100vh;
|
| padding: 20px;
|
| }
|
|
|
| .container {
|
| max-width: 1200px;
|
| margin: 0 auto;
|
| }
|
|
|
| .header {
|
| background: white;
|
| padding: 30px;
|
| border-radius: 15px;
|
| box-shadow: 0 10px 40px rgba(0,0,0,0.1);
|
| margin-bottom: 30px;
|
| text-align: center;
|
| }
|
|
|
| .header h1 {
|
| color: #667eea;
|
| font-size: 2.5em;
|
| margin-bottom: 10px;
|
| }
|
|
|
| .header p {
|
| color: #666;
|
| font-size: 1.1em;
|
| }
|
|
|
| .authors {
|
| color: #888;
|
| font-size: 0.9em;
|
| margin-top: 10px;
|
| }
|
|
|
| .main-content {
|
| display: grid;
|
| grid-template-columns: 1fr 1fr;
|
| gap: 20px;
|
| }
|
|
|
| .card {
|
| background: white;
|
| padding: 25px;
|
| border-radius: 15px;
|
| box-shadow: 0 10px 40px rgba(0,0,0,0.1);
|
| }
|
|
|
| .card h2 {
|
| color: #667eea;
|
| margin-bottom: 20px;
|
| font-size: 1.5em;
|
| }
|
|
|
| .form-group {
|
| margin-bottom: 20px;
|
| }
|
|
|
| label {
|
| display: block;
|
| margin-bottom: 8px;
|
| color: #333;
|
| font-weight: 600;
|
| }
|
|
|
| input[type="file"],
|
| input[type="number"],
|
| select {
|
| width: 100%;
|
| padding: 12px;
|
| border: 2px solid #e0e0e0;
|
| border-radius: 8px;
|
| font-size: 1em;
|
| transition: border-color 0.3s;
|
| }
|
|
|
| input:focus, select:focus {
|
| outline: none;
|
| border-color: #667eea;
|
| }
|
|
|
| .checkbox-group {
|
| display: flex;
|
| align-items: center;
|
| gap: 10px;
|
| }
|
|
|
| input[type="checkbox"] {
|
| width: 20px;
|
| height: 20px;
|
| cursor: pointer;
|
| }
|
|
|
| .btn {
|
| background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
| color: white;
|
| padding: 15px 30px;
|
| border: none;
|
| border-radius: 8px;
|
| font-size: 1.1em;
|
| font-weight: 600;
|
| cursor: pointer;
|
| width: 100%;
|
| transition: transform 0.2s, box-shadow 0.2s;
|
| }
|
|
|
| .btn:hover {
|
| transform: translateY(-2px);
|
| box-shadow: 0 5px 20px rgba(102, 126, 234, 0.4);
|
| }
|
|
|
| .btn:disabled {
|
| opacity: 0.5;
|
| cursor: not-allowed;
|
| }
|
|
|
| .job-list {
|
| max-height: 400px;
|
| overflow-y: auto;
|
| }
|
|
|
| .job-item {
|
| background: #f8f9fa;
|
| padding: 15px;
|
| border-radius: 8px;
|
| margin-bottom: 15px;
|
| border-left: 4px solid #667eea;
|
| }
|
|
|
| .job-header {
|
| display: flex;
|
| justify-content: space-between;
|
| align-items: center;
|
| margin-bottom: 10px;
|
| }
|
|
|
| .job-id {
|
| font-weight: 600;
|
| color: #333;
|
| }
|
|
|
| .status-badge {
|
| padding: 5px 15px;
|
| border-radius: 20px;
|
| font-size: 0.85em;
|
| font-weight: 600;
|
| }
|
|
|
| .status-pending {
|
| background: #ffeaa7;
|
| color: #d63031;
|
| }
|
|
|
| .status-running {
|
| background: #74b9ff;
|
| color: #0984e3;
|
| }
|
|
|
| .status-completed {
|
| background: #55efc4;
|
| color: #00b894;
|
| }
|
|
|
| .progress-bar {
|
| width: 100%;
|
| height: 8px;
|
| background: #e0e0e0;
|
| border-radius: 4px;
|
| overflow: hidden;
|
| margin-top: 10px;
|
| }
|
|
|
| .progress-fill {
|
| height: 100%;
|
| background: linear-gradient(90deg, #667eea 0%, #764ba2 100%);
|
| transition: width 0.3s ease;
|
| }
|
|
|
| .stats {
|
| display: grid;
|
| grid-template-columns: repeat(3, 1fr);
|
| gap: 15px;
|
| margin-top: 20px;
|
| }
|
|
|
| .stat-card {
|
| background: #f8f9fa;
|
| padding: 15px;
|
| border-radius: 8px;
|
| text-align: center;
|
| }
|
|
|
| .stat-value {
|
| font-size: 2em;
|
| font-weight: 700;
|
| color: #667eea;
|
| }
|
|
|
| .stat-label {
|
| color: #666;
|
| font-size: 0.9em;
|
| margin-top: 5px;
|
| }
|
|
|
| .notification {
|
| position: fixed;
|
| top: 20px;
|
| right: 20px;
|
| background: white;
|
| padding: 20px;
|
| border-radius: 8px;
|
| box-shadow: 0 5px 20px rgba(0,0,0,0.2);
|
| display: none;
|
| min-width: 300px;
|
| animation: slideIn 0.3s ease;
|
| }
|
|
|
| @keyframes slideIn {
|
| from {
|
| transform: translateX(400px);
|
| opacity: 0;
|
| }
|
| to {
|
| transform: translateX(0);
|
| opacity: 1;
|
| }
|
| }
|
|
|
| .notification.success {
|
| border-left: 4px solid #00b894;
|
| }
|
|
|
| .notification.error {
|
| border-left: 4px solid #d63031;
|
| }
|
|
|
| .footer {
|
| text-align: center;
|
| color: white;
|
| margin-top: 30px;
|
| padding: 20px;
|
| }
|
|
|
| .full-width {
|
| grid-column: 1 / -1;
|
| }
|
| </style>
|
| </head>
|
| <body>
|
| <div class="container">
|
| <div class="header">
|
| <h1>𧬠Docking@HOME</h1>
|
| <p>Distributed Molecular Docking Platform</p>
|
| <div class="authors">
|
| OpenPeer AI Β· Riemann Computing Inc. Β· Bleunomics Β· Andrew Magdy Kamal
|
| </div>
|
| </div>
|
|
|
| <div class="main-content">
|
| <div class="card">
|
| <h2>π€ Submit Docking Job</h2>
|
| <form id="dockingForm">
|
| <div class="form-group">
|
| <label>Ligand File (PDBQT)</label>
|
| <input type="file" id="ligandFile" accept=".pdbqt,.pdb" required>
|
| </div>
|
|
|
| <div class="form-group">
|
| <label>Receptor File (PDBQT)</label>
|
| <input type="file" id="receptorFile" accept=".pdbqt,.pdb" required>
|
| </div>
|
|
|
| <div class="form-group">
|
| <label>Number of Runs</label>
|
| <input type="number" id="numRuns" value="100" min="1" max="1000" required>
|
| </div>
|
|
|
| <div class="form-group checkbox-group">
|
| <input type="checkbox" id="useGPU" checked>
|
| <label for="useGPU">Use GPU Acceleration</label>
|
| </div>
|
|
|
| <button type="submit" class="btn" id="submitBtn">
|
| π Start Docking
|
| </button>
|
| </form>
|
| </div>
|
|
|
| <div class="card">
|
| <h2>π Active Jobs</h2>
|
| <div class="job-list" id="jobList">
|
| <p style="text-align: center; color: #999; padding: 20px;">
|
| No jobs yet. Submit a docking job to get started!
|
| </p>
|
| </div>
|
| </div>
|
|
|
| <div class="card full-width">
|
| <h2>π System Statistics</h2>
|
| <div class="stats">
|
| <div class="stat-card">
|
| <div class="stat-value" id="totalJobs">0</div>
|
| <div class="stat-label">Total Jobs</div>
|
| </div>
|
| <div class="stat-card">
|
| <div class="stat-value" id="completedJobs">0</div>
|
| <div class="stat-label">Completed</div>
|
| </div>
|
| <div class="stat-card">
|
| <div class="stat-value" id="avgTime">0s</div>
|
| <div class="stat-label">Avg. Time</div>
|
| </div>
|
| </div>
|
| </div>
|
| </div>
|
|
|
| <div class="footer">
|
| <p>Support: andrew@bleunomics.com | Issues: <a href="https://huggingface.co/OpenPeerAI/DockingAtHOME/discussions" style="color: white;">HuggingFace</a></p>
|
| </div>
|
| </div>
|
|
|
| <div class="notification" id="notification">
|
| <div id="notificationMessage"></div>
|
| </div>
|
|
|
| <script>
|
| const API_BASE = window.location.origin;
|
|
|
| // WebSocket connection for real-time updates
|
| let ws = null;
|
|
|
| function connectWebSocket() {
|
| ws = new WebSocket(`ws://${window.location.host}/ws`);
|
|
|
| ws.onmessage = (event) => {
|
| const data = JSON.parse(event.data);
|
| updateJobList();
|
| updateStats();
|
| };
|
|
|
| ws.onerror = () => {
|
| setTimeout(connectWebSocket, 5000);
|
| };
|
| }
|
|
|
| // Submit docking job
|
| document.getElementById('dockingForm').addEventListener('submit', async (e) => {
|
| e.preventDefault();
|
|
|
| const ligandFile = document.getElementById('ligandFile').files[0];
|
| const receptorFile = document.getElementById('receptorFile').files[0];
|
| const numRuns = document.getElementById('numRuns').value;
|
| const useGPU = document.getElementById('useGPU').checked;
|
|
|
| if (!ligandFile || !receptorFile) {
|
| showNotification('Please select both ligand and receptor files', 'error');
|
| return;
|
| }
|
|
|
| const submitBtn = document.getElementById('submitBtn');
|
| submitBtn.disabled = true;
|
| submitBtn.textContent = 'β³ Uploading...';
|
|
|
| try {
|
| // Upload ligand
|
| const ligandFormData = new FormData();
|
| ligandFormData.append('file', ligandFile);
|
| const ligandResponse = await fetch(`${API_BASE}/upload`, {
|
| method: 'POST',
|
| body: ligandFormData
|
| });
|
| const ligandData = await ligandResponse.json();
|
|
|
| // Upload receptor
|
| const receptorFormData = new FormData();
|
| receptorFormData.append('file', receptorFile);
|
| const receptorResponse = await fetch(`${API_BASE}/upload`, {
|
| method: 'POST',
|
| body: receptorFormData
|
| });
|
| const receptorData = await receptorResponse.json();
|
|
|
| // Submit job
|
| const jobResponse = await fetch(`${API_BASE}/api/jobs`, {
|
| method: 'POST',
|
| headers: {
|
| 'Content-Type': 'application/json'
|
| },
|
| body: JSON.stringify({
|
| ligand_file: ligandData.filename,
|
| receptor_file: receptorData.filename,
|
| num_runs: parseInt(numRuns),
|
| use_gpu: useGPU
|
| })
|
| });
|
|
|
| const jobData = await jobResponse.json();
|
|
|
| showNotification(`Job submitted successfully! ID: ${jobData.job_id}`, 'success');
|
|
|
| // Reset form
|
| document.getElementById('dockingForm').reset();
|
|
|
| // Update job list
|
| updateJobList();
|
| updateStats();
|
|
|
| } catch (error) {
|
| showNotification('Error submitting job: ' + error.message, 'error');
|
| } finally {
|
| submitBtn.disabled = false;
|
| submitBtn.textContent = 'π Start Docking';
|
| }
|
| });
|
|
|
| // Update job list
|
| async function updateJobList() {
|
| try {
|
| const response = await fetch(`${API_BASE}/api/jobs`);
|
| const jobs = await response.json();
|
|
|
| const jobList = document.getElementById('jobList');
|
|
|
| if (jobs.length === 0) {
|
| jobList.innerHTML = '<p style="text-align: center; color: #999; padding: 20px;">No jobs yet. Submit a docking job to get started!</p>';
|
| return;
|
| }
|
|
|
| jobList.innerHTML = jobs.map(job => `
|
| <div class="job-item">
|
| <div class="job-header">
|
| <span class="job-id">${job.job_id}</span>
|
| <span class="status-badge status-${job.status}">${job.status.toUpperCase()}</span>
|
| </div>
|
| <div style="font-size: 0.9em; color: #666;">
|
| <div>Ligand: ${job.ligand_file}</div>
|
| <div>Receptor: ${job.receptor_file}</div>
|
| <div>Runs: ${job.num_runs} | GPU: ${job.use_gpu ? 'Yes' : 'No'}</div>
|
| </div>
|
| <div class="progress-bar">
|
| <div class="progress-fill" style="width: ${job.progress * 100}%"></div>
|
| </div>
|
| <div style="margin-top: 5px; font-size: 0.85em; color: #666;">
|
| Progress: ${(job.progress * 100).toFixed(1)}%
|
| </div>
|
| </div>
|
| `).join('');
|
|
|
| } catch (error) {
|
| console.error('Error updating job list:', error);
|
| }
|
| }
|
|
|
| // Update statistics
|
| async function updateStats() {
|
| try {
|
| const response = await fetch(`${API_BASE}/api/stats`);
|
| const stats = await response.json();
|
|
|
| document.getElementById('totalJobs').textContent = stats.total_jobs;
|
| document.getElementById('completedJobs').textContent = stats.completed_jobs;
|
| document.getElementById('avgTime').textContent = stats.avg_time + 's';
|
|
|
| } catch (error) {
|
| console.error('Error updating stats:', error);
|
| }
|
| }
|
|
|
| // Show notification
|
| function showNotification(message, type) {
|
| const notification = document.getElementById('notification');
|
| const messageElement = document.getElementById('notificationMessage');
|
|
|
| messageElement.textContent = message;
|
| notification.className = `notification ${type}`;
|
| notification.style.display = 'block';
|
|
|
| setTimeout(() => {
|
| notification.style.display = 'none';
|
| }, 5000);
|
| }
|
|
|
| // Initialize
|
| connectWebSocket();
|
| updateJobList();
|
| updateStats();
|
|
|
| // Refresh job list every 2 seconds
|
| setInterval(() => {
|
| updateJobList();
|
| updateStats();
|
| }, 2000);
|
| </script>
|
| </body>
|
| </html>
|
| """
|
| return HTMLResponse(content=html_content)
|
|
|
|
|
| @app.post("/upload")
|
| async def upload_file(file: UploadFile = File(...)):
|
| """Upload ligand or receptor file"""
|
| try:
|
| file_path = UPLOAD_DIR / file.filename
|
|
|
| with open(file_path, "wb") as f:
|
| content = await file.read()
|
| f.write(content)
|
|
|
| return {"filename": file.filename, "path": str(file_path)}
|
|
|
| except Exception as e:
|
| raise HTTPException(status_code=500, detail=str(e))
|
|
|
|
|
| @app.post("/api/jobs")
|
| async def create_job(
|
| ligand_file: str,
|
| receptor_file: str,
|
| num_runs: int = 100,
|
| use_gpu: bool = True,
|
| job_name: Optional[str] = None
|
| ):
|
| """Create a new docking job with real AutoDock integration"""
|
| try:
|
|
|
| job_id = await job_manager.submit_job(
|
| ligand_file=ligand_file,
|
| receptor_file=receptor_file,
|
| num_runs=num_runs,
|
| use_gpu=use_gpu,
|
| job_name=job_name
|
| )
|
|
|
|
|
| asyncio.create_task(broadcast_job_updates(job_id))
|
|
|
| return job_manager.get_job(job_id)
|
|
|
| except Exception as e:
|
| raise HTTPException(status_code=500, detail=str(e))
|
|
|
|
|
| @app.get("/api/jobs")
|
| async def get_jobs():
|
| """Get all jobs"""
|
| return job_manager.get_all_jobs()
|
|
|
|
|
| @app.get("/api/jobs/{job_id}")
|
| async def get_job(job_id: str):
|
| """Get specific job"""
|
| job = job_manager.get_job(job_id)
|
|
|
| if not job:
|
| raise HTTPException(status_code=404, detail="Job not found")
|
|
|
| return job
|
|
|
|
|
| @app.get("/api/stats")
|
| async def get_stats():
|
| """Get system statistics"""
|
| return job_manager.get_stats()
|
|
|
|
|
| @app.websocket("/ws")
|
| async def websocket_endpoint(websocket: WebSocket):
|
| """WebSocket for real-time updates"""
|
| await websocket.accept()
|
| active_websockets.append(websocket)
|
|
|
| try:
|
| while True:
|
| data = await websocket.receive_text()
|
|
|
| await websocket.send_json({"status": "connected"})
|
| except WebSocketDisconnect:
|
| active_websockets.remove(websocket)
|
| except Exception as e:
|
| if websocket in active_websockets:
|
| active_websockets.remove(websocket)
|
|
|
|
|
| async def broadcast_job_updates(job_id: str):
|
| """Broadcast job progress to all connected WebSocket clients"""
|
|
|
| while True:
|
| await asyncio.sleep(0.5)
|
|
|
| job = job_manager.get_job(job_id)
|
|
|
| if not job:
|
| break
|
|
|
|
|
| for ws in active_websockets[:]:
|
| try:
|
| await ws.send_json({
|
| "type": "job_update",
|
| "job_id": job_id,
|
| "status": job["status"],
|
| "progress": job["progress"]
|
| })
|
| except Exception:
|
|
|
| if ws in active_websockets:
|
| active_websockets.remove(ws)
|
|
|
|
|
| if job["status"] in ["completed", "failed"]:
|
| break
|
|
|
|
|
| @app.on_event("startup")
|
| async def startup_event():
|
| """Initialize the server on startup"""
|
| await initialize_server()
|
|
|
|
|
| def start_gui(host: str = "localhost", port: int = 8080):
|
| """Start the GUI server with AutoDock integration"""
|
| print(f"""
|
| βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| β Docking@HOME GUI Server β
|
| β Real AutoDock Integration with GPU Support β
|
| β β
|
| β π Server: http://{host}:{port} β
|
| β 𧬠AutoDock: Enabled (GPU acceleration supported) β
|
| β π§ Support: andrew@bleunomics.com β
|
| β π€ Issues: https://huggingface.co/OpenPeerAI/DockingAtHOME β
|
| β β
|
| β Open your browser to start docking! β
|
| βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| """)
|
|
|
| uvicorn.run(app, host=host, port=port, log_level="info")
|
|
|
|
|
| if __name__ == "__main__":
|
| start_gui()
|
|
|