Spaces:
Runtime error
Runtime error
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Hugging Face Experiment Control Panel</title> | |
| <link rel="icon" href="/public/favicon.ico" type="image/x-icon"> | |
| <meta name="description" content="Remote control panel and monitoring view for Hugging Face Proxy experiment workflows"> | |
| <!-- Preload fonts and resources --> | |
| <link rel="preconnect" href="https://fonts.googleapis.com"> | |
| <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> | |
| <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&display=swap" rel="stylesheet"> | |
| <style> | |
| * { | |
| box-sizing: border-box; | |
| } | |
| body { | |
| margin: 0; | |
| font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, | |
| Ubuntu, Cantarell, 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif; | |
| -webkit-font-smoothing: antialiased; | |
| -moz-osx-font-smoothing: grayscale; | |
| background-color: #f8f9fa; | |
| color: #343a40; | |
| } | |
| .app-container { | |
| display: flex; | |
| flex-direction: column; | |
| min-height: 100vh; | |
| background-color: #f8f9fa; | |
| } | |
| .app-header { | |
| background-color: #343a40; | |
| color: white; | |
| padding: 1rem 2rem; | |
| box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); | |
| } | |
| .header-logo { | |
| display: flex; | |
| align-items: center; | |
| } | |
| .logo { | |
| height: 40px; | |
| margin-right: 15px; | |
| } | |
| .app-header h1 { | |
| margin: 0; | |
| font-size: 1.5rem; | |
| font-weight: 600; | |
| } | |
| .app-content { | |
| display: flex; | |
| flex: 1; | |
| padding: 20px; | |
| gap: 20px; | |
| } | |
| .left-panel, .right-panel { | |
| width: 50%; | |
| display: flex; | |
| flex-direction: column; | |
| gap: 20px; | |
| } | |
| .panel { | |
| background-color: white; | |
| border-radius: 8px; | |
| box-shadow: 0 2px 10px rgba(0, 0, 0, 0.05); | |
| padding: 20px; | |
| } | |
| .panel-header { | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| margin-bottom: 15px; | |
| border-bottom: 1px solid #e9ecef; | |
| padding-bottom: 10px; | |
| } | |
| .panel-title { | |
| font-size: 18px; | |
| font-weight: 600; | |
| color: #343a40; | |
| margin: 0; | |
| } | |
| .server-status { | |
| padding: 15px; | |
| border-radius: 6px; | |
| background-color: #f8f9fa; | |
| margin-bottom: 20px; | |
| } | |
| .status-indicator { | |
| display: flex; | |
| align-items: center; | |
| margin-bottom: 8px; | |
| } | |
| .status-dot { | |
| width: 12px; | |
| height: 12px; | |
| border-radius: 50%; | |
| margin-right: 8px; | |
| } | |
| .status-dot.online { | |
| background-color: #28a745; | |
| box-shadow: 0 0 8px rgba(40, 167, 69, 0.6); | |
| animation: pulse 2s infinite; | |
| } | |
| .status-dot.offline { | |
| background-color: #dc3545; | |
| } | |
| .status-text { | |
| font-weight: 500; | |
| } | |
| .workflow-dropzone { | |
| padding: 30px; | |
| border: 2px dashed #ccc; | |
| border-radius: 6px; | |
| background-color: #fafafa; | |
| text-align: center; | |
| cursor: pointer; | |
| margin-bottom: 15px; | |
| transition: all 0.2s ease; | |
| } | |
| .workflow-dropzone:hover { | |
| border-color: #007bff; | |
| background-color: rgba(0, 123, 255, 0.05); | |
| } | |
| .workflow-json { | |
| width: 100%; | |
| height: 200px; | |
| padding: 12px; | |
| border: 1px solid #ced4da; | |
| border-radius: 4px; | |
| font-family: monospace; | |
| font-size: 14px; | |
| line-height: 1.5; | |
| resize: vertical; | |
| } | |
| .action-buttons { | |
| display: flex; | |
| justify-content: flex-end; | |
| gap: 10px; | |
| margin-top: 15px; | |
| } | |
| button { | |
| padding: 8px 16px; | |
| border: none; | |
| border-radius: 4px; | |
| cursor: pointer; | |
| font-weight: 500; | |
| transition: background-color 0.2s; | |
| } | |
| button:disabled { | |
| opacity: 0.6; | |
| cursor: not-allowed; | |
| } | |
| .primary-button { | |
| background-color: #007bff; | |
| color: white; | |
| } | |
| .primary-button:hover:not(:disabled) { | |
| background-color: #0069d9; | |
| } | |
| .secondary-button { | |
| background-color: #6c757d; | |
| color: white; | |
| } | |
| .secondary-button:hover:not(:disabled) { | |
| background-color: #5a6268; | |
| } | |
| .run-status { | |
| padding: 20px; | |
| } | |
| .run-status-header { | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| margin-bottom: 20px; | |
| } | |
| .status-badge { | |
| padding: 4px 8px; | |
| border-radius: 4px; | |
| font-size: 12px; | |
| font-weight: 500; | |
| text-transform: uppercase; | |
| } | |
| .status-badge.pending { | |
| background-color: #ffc107; | |
| color: #212529; | |
| } | |
| .status-badge.running { | |
| background-color: #17a2b8; | |
| color: white; | |
| } | |
| .status-badge.completed { | |
| background-color: #28a745; | |
| color: white; | |
| } | |
| .status-badge.failed { | |
| background-color: #dc3545; | |
| color: white; | |
| } | |
| .steps-list { | |
| display: flex; | |
| flex-direction: column; | |
| gap: 5px; | |
| } | |
| .step-item { | |
| display: flex; | |
| padding: 5px 0; | |
| } | |
| .step-indicator { | |
| display: flex; | |
| flex-direction: column; | |
| align-items: center; | |
| margin-right: 15px; | |
| } | |
| .step-number { | |
| width: 28px; | |
| height: 28px; | |
| border-radius: 50%; | |
| background-color: #e9ecef; | |
| display: flex; | |
| justify-content: center; | |
| align-items: center; | |
| font-weight: 600; | |
| font-size: 14px; | |
| margin-bottom: 5px; | |
| z-index: 1; | |
| } | |
| .step-item.completed .step-number { | |
| background-color: #28a745; | |
| color: white; | |
| } | |
| .step-item.running .step-number { | |
| background-color: #17a2b8; | |
| color: white; | |
| animation: pulse-blue 2s infinite; | |
| } | |
| .step-line { | |
| width: 2px; | |
| flex-grow: 1; | |
| background-color: #e9ecef; | |
| } | |
| .step-item:last-child .step-line { | |
| display: none; | |
| } | |
| .step-content { | |
| flex-grow: 1; | |
| padding: 0 0 15px; | |
| } | |
| .step-header { | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| margin-bottom: 5px; | |
| } | |
| .step-label { | |
| font-weight: 500; | |
| font-size: 16px; | |
| } | |
| .step-status { | |
| font-size: 12px; | |
| padding: 2px 6px; | |
| border-radius: 4px; | |
| text-transform: uppercase; | |
| } | |
| .history-list { | |
| max-height: 300px; | |
| overflow-y: auto; | |
| } | |
| .history-item { | |
| padding: 12px 15px; | |
| border-bottom: 1px solid #e9ecef; | |
| cursor: pointer; | |
| transition: background-color 0.2s; | |
| } | |
| .history-item:hover { | |
| background-color: #e9ecef; | |
| } | |
| .history-item-content { | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| margin-bottom: 5px; | |
| } | |
| .history-item-name { | |
| font-weight: 500; | |
| } | |
| .history-item-timestamp { | |
| font-size: 12px; | |
| color: #6c757d; | |
| } | |
| .console-header { | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| padding: 12px 15px; | |
| background-color: #343a40; | |
| cursor: pointer; | |
| user-select: none; | |
| color: white; | |
| border-radius: 4px 4px 0 0; | |
| } | |
| .console-title { | |
| display: flex; | |
| align-items: center; | |
| } | |
| .console-title h3 { | |
| margin: 0; | |
| font-size: 16px; | |
| color: #f8f9fa; | |
| } | |
| .console-logs { | |
| background-color: #212529; | |
| color: #f8f9fa; | |
| padding: 15px; | |
| font-family: monospace; | |
| font-size: 14px; | |
| border-radius: 0 0 4px 4px; | |
| max-height: 200px; | |
| overflow-y: auto; | |
| } | |
| .log-entry { | |
| margin-bottom: 8px; | |
| padding-left: 10px; | |
| border-left: 3px solid #0dcaf0; | |
| } | |
| .app-footer { | |
| background-color: #343a40; | |
| color: #adb5bd; | |
| text-align: center; | |
| padding: 10px; | |
| font-size: 14px; | |
| } | |
| @keyframes pulse { | |
| 0% { | |
| box-shadow: 0 0 0 0 rgba(40, 167, 69, 0.6); | |
| } | |
| 70% { | |
| box-shadow: 0 0 0 6px rgba(40, 167, 69, 0); | |
| } | |
| 100% { | |
| box-shadow: 0 0 0 0 rgba(40, 167, 69, 0); | |
| } | |
| } | |
| @keyframes pulse-blue { | |
| 0% { | |
| box-shadow: 0 0 0 0 rgba(23, 162, 184, 0.6); | |
| } | |
| 70% { | |
| box-shadow: 0 0 0 6px rgba(23, 162, 184, 0); | |
| } | |
| 100% { | |
| box-shadow: 0 0 0 0 rgba(23, 162, 184, 0); | |
| } | |
| } | |
| @media (max-width: 1024px) { | |
| .app-content { | |
| flex-direction: column; | |
| } | |
| .left-panel, .right-panel { | |
| width: 100%; | |
| } | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <noscript>You need to enable JavaScript to run this application.</noscript> | |
| <div class="app-container"> | |
| <header class="app-header"> | |
| <div class="header-logo"> | |
| <img src="/public/logo.png" alt="Logo" class="logo" /> | |
| <h1>Hugging Face Experiment Control Panel</h1> | |
| </div> | |
| </header> | |
| <main class="app-content"> | |
| <div class="left-panel"> | |
| <!-- Server Status Panel --> | |
| <div class="panel"> | |
| <div class="panel-header"> | |
| <h2 class="panel-title">Server Status</h2> | |
| </div> | |
| <div class="server-status" id="server-status"> | |
| <div class="status-indicator"> | |
| <span class="status-dot"></span> | |
| <span class="status-text">Checking status...</span> | |
| </div> | |
| <div class="status-latency"></div> | |
| <div class="last-checked">Last check: Never</div> | |
| </div> | |
| <button id="check-status" class="secondary-button">Check Status</button> | |
| </div> | |
| <!-- Workflow Sender Panel --> | |
| <div class="panel"> | |
| <div class="panel-header"> | |
| <h2 class="panel-title">Workflow Sender</h2> | |
| </div> | |
| <div class="workflow-dropzone" id="workflow-dropzone"> | |
| <p>Drop JSON file here or click to upload</p> | |
| <small>Supports .json workflow files</small> | |
| </div> | |
| <textarea id="workflow-json" class="workflow-json" placeholder="Paste your JSON workflow here..."></textarea> | |
| <div class="action-buttons"> | |
| <button id="load-sample" class="secondary-button">Load Sample</button> | |
| <button id="reset-workflow" class="secondary-button">Reset</button> | |
| <button id="send-workflow" class="primary-button" disabled>Send Workflow</button> | |
| </div> | |
| </div> | |
| <!-- History Panel --> | |
| <div class="panel"> | |
| <div class="panel-header"> | |
| <h2 class="panel-title">History</h2> | |
| </div> | |
| <div id="history-list" class="history-list"> | |
| <div class="empty-history"> | |
| <p>No history records</p> | |
| <small>Workflow runs will appear here</small> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="right-panel"> | |
| <!-- Run Status Panel --> | |
| <div class="panel"> | |
| <div class="panel-header"> | |
| <h2 class="panel-title">Execution Status</h2> | |
| </div> | |
| <div id="run-status" class="run-status"> | |
| <div class="empty-message"> | |
| <p>No workflow execution</p> | |
| <small>Send a workflow to see execution progress here</small> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Debug Console --> | |
| <div class="panel"> | |
| <div class="console-header"> | |
| <div class="console-title"> | |
| <h3>Debug Console</h3> | |
| </div> | |
| </div> | |
| <div id="console-logs" class="console-logs"> | |
| <div class="log-entry"> | |
| System initialized. Ready to send workflows. | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </main> | |
| <footer class="app-footer"> | |
| <p>© 2023 Hugging Face Proxy Control Panel</p> | |
| </footer> | |
| </div> | |
| <script> | |
| // Global state | |
| const state = { | |
| serverStatus: null, | |
| currentWorkflow: null, | |
| currentRun: null, | |
| history: [], | |
| logs: [ | |
| { | |
| message: "System initialized. Ready to send workflows.", | |
| timestamp: new Date(), | |
| type: "info" | |
| } | |
| ], | |
| statusCheckInterval: null | |
| }; | |
| // DOM Elements | |
| const serverStatusEl = document.getElementById('server-status'); | |
| const checkStatusBtn = document.getElementById('check-status'); | |
| const workflowDropzone = document.getElementById('workflow-dropzone'); | |
| const workflowJsonEl = document.getElementById('workflow-json'); | |
| const loadSampleBtn = document.getElementById('load-sample'); | |
| const resetWorkflowBtn = document.getElementById('reset-workflow'); | |
| const sendWorkflowBtn = document.getElementById('send-workflow'); | |
| const historyListEl = document.getElementById('history-list'); | |
| const runStatusEl = document.getElementById('run-status'); | |
| const consoleLogsEl = document.getElementById('console-logs'); | |
| // Check server status | |
| async function checkServerStatus() { | |
| try { | |
| const response = await fetch('/api/health'); | |
| const data = await response.json(); | |
| state.serverStatus = data; | |
| updateServerStatusUI(); | |
| addLogEntry({ | |
| message: `Status check: Lab server is ${data.status === 'online' ? 'online' : 'offline'}`, | |
| timestamp: new Date(), | |
| type: data.status === 'online' ? 'info' : 'error' | |
| }); | |
| return data; | |
| } catch (error) { | |
| console.error('Failed to check server status:', error); | |
| state.serverStatus = { | |
| status: 'offline', | |
| message: 'Connection error', | |
| latency: 0 | |
| }; | |
| updateServerStatusUI(); | |
| addLogEntry({ | |
| message: `Status check failed: ${error.message}`, | |
| timestamp: new Date(), | |
| type: 'error' | |
| }); | |
| } | |
| } | |
| // Update server status UI | |
| function updateServerStatusUI() { | |
| const status = state.serverStatus; | |
| if (!status) return; | |
| serverStatusEl.innerHTML = ` | |
| <div class="status-indicator"> | |
| <span class="status-dot ${status.status}"></span> | |
| <span class="status-text"> | |
| ${status.status === 'online' ? 'Online' : 'Offline'} | |
| </span> | |
| </div> | |
| ${status.message ? `<div class="status-message">${status.message}</div>` : ''} | |
| ${status.latency ? `<div class="status-latency">Latency: ${status.latency}ms</div>` : ''} | |
| <div class="last-checked"> | |
| Last check: ${new Date().toLocaleTimeString()} | |
| </div> | |
| `; | |
| } | |
| // Load sample workflow | |
| async function loadSampleWorkflow() { | |
| try { | |
| const response = await fetch('/api/sample_workflow'); | |
| const workflow = await response.json(); | |
| state.currentWorkflow = workflow; | |
| workflowJsonEl.value = JSON.stringify(workflow, null, 2); | |
| sendWorkflowBtn.disabled = false; | |
| addLogEntry({ | |
| message: "Sample workflow loaded", | |
| timestamp: new Date(), | |
| type: "info" | |
| }); | |
| } catch (error) { | |
| console.error('Failed to load sample workflow:', error); | |
| addLogEntry({ | |
| message: `Failed to load sample workflow: ${error.message}`, | |
| timestamp: new Date(), | |
| type: "error" | |
| }); | |
| } | |
| } | |
| // Reset workflow | |
| function resetWorkflow() { | |
| state.currentWorkflow = null; | |
| workflowJsonEl.value = ''; | |
| sendWorkflowBtn.disabled = true; | |
| addLogEntry({ | |
| message: "Workflow reset", | |
| timestamp: new Date(), | |
| type: "info" | |
| }); | |
| } | |
| // Send workflow | |
| async function sendWorkflow() { | |
| if (!state.currentWorkflow) { | |
| addLogEntry({ | |
| message: "No valid workflow to send", | |
| timestamp: new Date(), | |
| type: "error" | |
| }); | |
| return; | |
| } | |
| try { | |
| addLogEntry({ | |
| message: "Sending workflow...", | |
| timestamp: new Date(), | |
| type: "info" | |
| }); | |
| const response = await fetch('/api/run_experiment', { | |
| method: 'POST', | |
| headers: { | |
| 'Content-Type': 'application/json', | |
| }, | |
| body: JSON.stringify(state.currentWorkflow), | |
| }); | |
| if (!response.ok) { | |
| throw new Error(`Server responded with ${response.status}`); | |
| } | |
| const result = await response.json(); | |
| state.currentRun = result; | |
| // Add to history | |
| state.history.unshift({ | |
| id: result.run_id, | |
| workflow: state.currentWorkflow, | |
| result: result, | |
| timestamp: new Date().toISOString() | |
| }); | |
| // Update UI | |
| updateHistoryUI(); | |
| updateRunStatusUI(); | |
| addLogEntry({ | |
| message: `Workflow run submitted: ${result.run_id}`, | |
| timestamp: new Date(), | |
| type: "info" | |
| }); | |
| // Start polling for status updates | |
| pollRunStatus(result.run_id); | |
| } catch (error) { | |
| console.error('Failed to send workflow:', error); | |
| addLogEntry({ | |
| message: `Failed to send workflow: ${error.message}`, | |
| timestamp: new Date(), | |
| type: "error" | |
| }); | |
| } | |
| } | |
| // Poll run status | |
| async function pollRunStatus(runId) { | |
| if (!runId) return; | |
| try { | |
| const response = await fetch(`/api/run_status/${runId}`); | |
| if (!response.ok) { | |
| throw new Error(`Server responded with ${response.status}`); | |
| } | |
| const result = await response.json(); | |
| // Update current run | |
| state.currentRun = result; | |
| // Update history item | |
| const historyItem = state.history.find(item => item.id === runId); | |
| if (historyItem) { | |
| historyItem.result = result; | |
| } | |
| // Update UI | |
| updateRunStatusUI(); | |
| updateHistoryUI(); | |
| // Continue polling if not completed | |
| if (result.status === 'running' || result.status === 'pending') { | |
| setTimeout(() => pollRunStatus(runId), 2000); // Poll every 2 seconds | |
| } else { | |
| addLogEntry({ | |
| message: `Workflow run ${result.status}: ${runId}`, | |
| timestamp: new Date(), | |
| type: result.status === 'completed' ? 'info' : 'error' | |
| }); | |
| } | |
| } catch (error) { | |
| console.error('Failed to get run status:', error); | |
| addLogEntry({ | |
| message: `Failed to get run status: ${error.message}`, | |
| timestamp: new Date(), | |
| type: "error" | |
| }); | |
| } | |
| } | |
| // Update history UI | |
| function updateHistoryUI() { | |
| if (state.history.length === 0) { | |
| historyListEl.innerHTML = ` | |
| <div class="empty-history"> | |
| <p>No history records</p> | |
| <small>Workflow runs will appear here</small> | |
| </div> | |
| `; | |
| return; | |
| } | |
| historyListEl.innerHTML = state.history.map(item => ` | |
| <div class="history-item" data-id="${item.id}"> | |
| <div class="history-item-content"> | |
| <div class="history-item-name"> | |
| ${item.workflow.name || 'Unnamed Workflow'} | |
| </div> | |
| <div class="history-item-timestamp"> | |
| ${new Date(item.timestamp).toLocaleTimeString()} | |
| </div> | |
| </div> | |
| <div class="history-item-stats"> | |
| <span>${item.workflow.nodes.length} nodes</span> | |
| <span>${item.workflow.edges.length} edges</span> | |
| <span class="status-badge ${item.result?.status || 'pending'}"> | |
| ${item.result?.status || 'pending'} | |
| </span> | |
| </div> | |
| </div> | |
| `).join(''); | |
| // Add event listeners | |
| document.querySelectorAll('.history-item').forEach(item => { | |
| item.addEventListener('click', () => { | |
| const id = item.dataset.id; | |
| const historyItem = state.history.find(h => h.id === id); | |
| if (historyItem) { | |
| state.currentRun = historyItem.result; | |
| state.currentWorkflow = historyItem.workflow; | |
| workflowJsonEl.value = JSON.stringify(historyItem.workflow, null, 2); | |
| sendWorkflowBtn.disabled = false; | |
| updateRunStatusUI(); | |
| } | |
| }); | |
| }); | |
| } | |
| // Update run status UI | |
| function updateRunStatusUI() { | |
| if (!state.currentRun) { | |
| runStatusEl.innerHTML = ` | |
| <div class="empty-message"> | |
| <p>No workflow execution</p> | |
| <small>Send a workflow to see execution progress here</small> | |
| </div> | |
| `; | |
| return; | |
| } | |
| const run = state.currentRun; | |
| runStatusEl.innerHTML = ` | |
| <div class="run-status-header"> | |
| <h3>Execution Status</h3> | |
| <div class="run-status-overview"> | |
| <span class="status-badge ${run.status}"> | |
| ${run.status} | |
| </span> | |
| <span class="run-id"> | |
| Run ID: ${run.run_id || 'N/A'} | |
| </span> | |
| </div> | |
| </div> | |
| ${(run.started_at || run.completed_at) ? ` | |
| <div class="run-timing"> | |
| ${run.started_at ? ` | |
| <span class="started-at"> | |
| Started: ${new Date(run.started_at).toLocaleString()} | |
| </span> | |
| ` : ''} | |
| ${run.completed_at ? ` | |
| <span class="completed-at"> | |
| Completed: ${new Date(run.completed_at).toLocaleString()} | |
| </span> | |
| ` : ''} | |
| </div> | |
| ` : ''} | |
| ${run.error ? ` | |
| <div class="run-error"> | |
| <div class="error-title">Error:</div> | |
| <div class="error-message">${run.error}</div> | |
| </div> | |
| ` : ''} | |
| <div class="steps-container"> | |
| ${run.steps.length > 0 ? ` | |
| <div class="steps-list"> | |
| ${run.steps.map((step, index) => ` | |
| <div class="step-item ${step.status}"> | |
| <div class="step-indicator"> | |
| <div class="step-number">${step.step_index + 1}</div> | |
| <div class="step-line"></div> | |
| </div> | |
| <div class="step-content"> | |
| <div class="step-header"> | |
| <div class="step-label">${step.label}</div> | |
| <div class="step-status ${step.status}"> | |
| ${step.status} | |
| </div> | |
| </div> | |
| ${step.error ? ` | |
| <div class="step-error">${step.error}</div> | |
| ` : ''} | |
| ${(step.started_at || step.completed_at) ? ` | |
| <div class="step-timing"> | |
| ${step.started_at ? ` | |
| <span class="step-started-at"> | |
| Started: ${new Date(step.started_at).toLocaleTimeString()} | |
| </span> | |
| ` : ''} | |
| ${step.completed_at ? ` | |
| <span class="step-completed-at"> | |
| Completed: ${new Date(step.completed_at).toLocaleTimeString()} | |
| </span> | |
| ` : ''} | |
| </div> | |
| ` : ''} | |
| </div> | |
| </div> | |
| `).join('')} | |
| </div> | |
| ` : ` | |
| <div class="no-steps"> | |
| <p>No step information available</p> | |
| </div> | |
| `} | |
| </div> | |
| `; | |
| } | |
| // Add log entry | |
| function addLogEntry(log) { | |
| state.logs.push(log); | |
| updateConsoleUI(); | |
| } | |
| // Update console UI | |
| function updateConsoleUI() { | |
| consoleLogsEl.innerHTML = state.logs.map(log => ` | |
| <div class="log-entry ${log.type}"> | |
| [${log.timestamp.toLocaleTimeString()}] ${log.message} | |
| </div> | |
| `).join(''); | |
| // Scroll to bottom | |
| consoleLogsEl.scrollTop = consoleLogsEl.scrollHeight; | |
| } | |
| // Parse workflow JSON | |
| function parseWorkflowJson(jsonText) { | |
| try { | |
| const workflow = JSON.parse(jsonText); | |
| state.currentWorkflow = workflow; | |
| sendWorkflowBtn.disabled = false; | |
| addLogEntry({ | |
| message: `Workflow loaded: ${workflow.name || 'Unnamed Workflow'}`, | |
| timestamp: new Date(), | |
| type: "info" | |
| }); | |
| return workflow; | |
| } catch (error) { | |
| state.currentWorkflow = null; | |
| sendWorkflowBtn.disabled = true; | |
| addLogEntry({ | |
| message: `Invalid JSON: ${error.message}`, | |
| timestamp: new Date(), | |
| type: "error" | |
| }); | |
| return null; | |
| } | |
| } | |
| // Handle file drop | |
| function handleFileDrop(e) { | |
| e.preventDefault(); | |
| workflowDropzone.classList.remove('dragging'); | |
| if (e.dataTransfer.files && e.dataTransfer.files.length > 0) { | |
| const file = e.dataTransfer.files[0]; | |
| if (file.type === 'application/json' || file.name.endsWith('.json')) { | |
| readJsonFile(file); | |
| } else { | |
| addLogEntry({ | |
| message: "Please upload JSON files only", | |
| timestamp: new Date(), | |
| type: "error" | |
| }); | |
| } | |
| } | |
| } | |
| // Read JSON file | |
| function readJsonFile(file) { | |
| const reader = new FileReader(); | |
| reader.onload = (e) => { | |
| try { | |
| const content = e.target.result; | |
| workflowJsonEl.value = content; | |
| parseWorkflowJson(content); | |
| } catch (error) { | |
| addLogEntry({ | |
| message: `Failed to parse JSON file: ${error.message}`, | |
| timestamp: new Date(), | |
| type: "error" | |
| }); | |
| } | |
| }; | |
| reader.onerror = () => { | |
| addLogEntry({ | |
| message: "Failed to read file", | |
| timestamp: new Date(), | |
| type: "error" | |
| }); | |
| }; | |
| reader.readAsText(file); | |
| } | |
| // Event Listeners | |
| checkStatusBtn.addEventListener('click', checkServerStatus); | |
| workflowDropzone.addEventListener('dragover', (e) => { | |
| e.preventDefault(); | |
| workflowDropzone.classList.add('dragging'); | |
| }); | |
| workflowDropzone.addEventListener('dragleave', (e) => { | |
| e.preventDefault(); | |
| workflowDropzone.classList.remove('dragging'); | |
| }); | |
| workflowDropzone.addEventListener('drop', handleFileDrop); | |
| workflowJsonEl.addEventListener('input', (e) => { | |
| if (e.target.value) { | |
| parseWorkflowJson(e.target.value); | |
| } else { | |
| state.currentWorkflow = null; | |
| sendWorkflowBtn.disabled = true; | |
| } | |
| }); | |
| loadSampleBtn.addEventListener('click', loadSampleWorkflow); | |
| resetWorkflowBtn.addEventListener('click', resetWorkflow); | |
| sendWorkflowBtn.addEventListener('click', sendWorkflow); | |
| // Initialize | |
| (async function init() { | |
| // Check server status | |
| await checkServerStatus(); | |
| // Set up periodic status check | |
| state.statusCheckInterval = setInterval(checkServerStatus, 30000); // Every 30 seconds | |
| // Update UI | |
| updateHistoryUI(); | |
| updateRunStatusUI(); | |
| updateConsoleUI(); | |
| addLogEntry({ | |
| message: "Application initialized and ready", | |
| timestamp: new Date(), | |
| type: "info" | |
| }); | |
| })(); | |
| </script> | |
| </body> | |
| </html> | |