HF_catalyst / static /index.html
SissiFeng's picture
Initial commit
cdb8847
<!DOCTYPE html>
<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>