test/moxie/admin/templates/comfyui.html
2026-03-24 04:04:58 +00:00

459 lines
22 KiB
HTML
Executable File

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>ComfyUI - MOXIE Admin</title>
<link rel="stylesheet" href="/{{ settings.admin_path }}/static/admin.css">
</head>
<body>
<nav class="navbar">
<div class="nav-brand">MOXIE Admin</div>
<div class="nav-links">
<a href="/{{ settings.admin_path }}/">Dashboard</a>
<a href="/{{ settings.admin_path }}/endpoints">Endpoints</a>
<a href="/{{ settings.admin_path }}/documents">Documents</a>
<a href="/{{ settings.admin_path }}/comfyui">ComfyUI</a>
</div>
</nav>
<main class="container">
<h1>ComfyUI Configuration</h1>
<p class="help-text">
Configure ComfyUI for image, video, and audio generation.
Upload workflows in <strong>API Format</strong> (enable Dev Mode in ComfyUI, then use "Save (API Format)").
</p>
<!-- Connection Settings -->
<section class="config-section">
<h2>Connection Settings</h2>
<form id="connection-form" class="form-inline">
<div class="form-group">
<label for="comfyui_host">ComfyUI URL</label>
<input type="text" id="comfyui_host" name="comfyui_host"
value="{{ config.get('comfyui_host', 'http://127.0.0.1:8188') }}"
placeholder="http://127.0.0.1:8188" style="width: 300px;">
</div>
<button type="submit" class="btn btn-primary btn-sm">Save & Test</button>
</form>
<div id="connection-status" class="status-indicator checking" style="margin-top: 0.5rem;">Checking...</div>
</section>
<!-- Workflow Tabs -->
<section class="workflow-section">
<div class="workflow-tabs">
<button class="tab-btn active" data-tab="image">Image</button>
<button class="tab-btn" data-tab="video">Video</button>
<button class="tab-btn" data-tab="audio">Audio</button>
</div>
<!-- Image Workflow Tab -->
<div id="image-tab" class="tab-content active">
<div class="workflow-header">
<h3>Image Generation Workflow</h3>
{% if workflows.image %}
<span class="badge success">Configured</span>
{% else %}
<span class="badge warning">Not Configured</span>
{% endif %}
</div>
<form id="image-workflow-form" class="workflow-form">
<!-- Workflow Upload -->
<div class="form-group">
<label>Workflow JSON (API Format)</label>
<div class="file-upload">
<input type="file" id="image-workflow-file" accept=".json">
<button type="button" class="btn btn-sm" onclick="uploadWorkflow('image')">Upload</button>
{% if workflows.image %}
<button type="button" class="btn btn-sm" onclick="viewWorkflow('image')">View JSON</button>
<button type="button" class="btn btn-danger btn-sm" onclick="deleteWorkflow('image')">Delete</button>
{% endif %}
</div>
</div>
<!-- Node ID Mappings -->
<div class="node-mappings">
<h4>Node ID Mappings</h4>
<p class="help-text">Map the node IDs from your workflow. Find these in ComfyUI or the workflow JSON.</p>
<div class="mapping-grid">
<div class="form-group">
<label for="image_prompt_node">Prompt Node ID</label>
<input type="text" id="image_prompt_node" name="image_prompt_node"
value="{{ config.get('image_prompt_node', '') }}"
placeholder="e.g., 6">
</div>
<div class="form-group">
<label for="image_negative_prompt_node">Negative Prompt Node ID</label>
<input type="text" id="image_negative_prompt_node" name="image_negative_prompt_node"
value="{{ config.get('image_negative_prompt_node', '') }}"
placeholder="e.g., 7">
</div>
<div class="form-group">
<label for="image_seed_node">Seed Node ID</label>
<input type="text" id="image_seed_node" name="image_seed_node"
value="{{ config.get('image_seed_node', '') }}"
placeholder="e.g., 3">
</div>
<div class="form-group">
<label for="image_steps_node">Steps Node ID</label>
<input type="text" id="image_steps_node" name="image_steps_node"
value="{{ config.get('image_steps_node', '') }}"
placeholder="e.g., 3">
</div>
<div class="form-group">
<label for="image_width_node">Width Node ID</label>
<input type="text" id="image_width_node" name="image_width_node"
value="{{ config.get('image_width_node', '') }}"
placeholder="e.g., 5">
</div>
<div class="form-group">
<label for="image_height_node">Height Node ID</label>
<input type="text" id="image_height_node" name="image_height_node"
value="{{ config.get('image_height_node', '') }}"
placeholder="e.g., 5">
</div>
<div class="form-group">
<label for="image_cfg_node">CFG Scale Node ID</label>
<input type="text" id="image_cfg_node" name="image_cfg_node"
value="{{ config.get('image_cfg_node', '') }}"
placeholder="e.g., 3">
</div>
<div class="form-group">
<label for="image_output_node">Output Node ID</label>
<input type="text" id="image_output_node" name="image_output_node"
value="{{ config.get('image_output_node', '') }}"
placeholder="e.g., 9">
</div>
</div>
<div class="form-group">
<label for="image_default_size">Default Size</label>
<input type="text" id="image_default_size" name="image_default_size"
value="{{ config.get('image_default_size', '512x512') }}"
placeholder="512x512">
</div>
<div class="form-group">
<label for="image_default_steps">Default Steps</label>
<input type="number" id="image_default_steps" name="image_default_steps"
value="{{ config.get('image_default_steps', 20) }}"
placeholder="20">
</div>
</div>
<button type="submit" class="btn btn-primary">Save Image Settings</button>
</form>
</div>
<!-- Video Workflow Tab -->
<div id="video-tab" class="tab-content">
<div class="workflow-header">
<h3>Video Generation Workflow</h3>
{% if workflows.video %}
<span class="badge success">Configured</span>
{% else %}
<span class="badge warning">Not Configured</span>
{% endif %}
</div>
<form id="video-workflow-form" class="workflow-form">
<div class="form-group">
<label>Workflow JSON (API Format)</label>
<div class="file-upload">
<input type="file" id="video-workflow-file" accept=".json">
<button type="button" class="btn btn-sm" onclick="uploadWorkflow('video')">Upload</button>
{% if workflows.video %}
<button type="button" class="btn btn-sm" onclick="viewWorkflow('video')">View JSON</button>
<button type="button" class="btn btn-danger btn-sm" onclick="deleteWorkflow('video')">Delete</button>
{% endif %}
</div>
</div>
<div class="node-mappings">
<h4>Node ID Mappings</h4>
<div class="mapping-grid">
<div class="form-group">
<label for="video_prompt_node">Prompt Node ID</label>
<input type="text" id="video_prompt_node" name="video_prompt_node"
value="{{ config.get('video_prompt_node', '') }}" placeholder="e.g., 6">
</div>
<div class="form-group">
<label for="video_seed_node">Seed Node ID</label>
<input type="text" id="video_seed_node" name="video_seed_node"
value="{{ config.get('video_seed_node', '') }}" placeholder="e.g., 3">
</div>
<div class="form-group">
<label for="video_frames_node">Frames Node ID</label>
<input type="text" id="video_frames_node" name="video_frames_node"
value="{{ config.get('video_frames_node', '') }}" placeholder="e.g., 10">
</div>
<div class="form-group">
<label for="video_output_node">Output Node ID</label>
<input type="text" id="video_output_node" name="video_output_node"
value="{{ config.get('video_output_node', '') }}" placeholder="e.g., 9">
</div>
</div>
<div class="form-group">
<label for="video_default_frames">Default Frames</label>
<input type="number" id="video_default_frames" name="video_default_frames"
value="{{ config.get('video_default_frames', 24) }}" placeholder="24">
</div>
</div>
<button type="submit" class="btn btn-primary">Save Video Settings</button>
</form>
</div>
<!-- Audio Workflow Tab -->
<div id="audio-tab" class="tab-content">
<div class="workflow-header">
<h3>Audio Generation Workflow</h3>
{% if workflows.audio %}
<span class="badge success">Configured</span>
{% else %}
<span class="badge warning">Not Configured</span>
{% endif %}
</div>
<form id="audio-workflow-form" class="workflow-form">
<div class="form-group">
<label>Workflow JSON (API Format)</label>
<div class="file-upload">
<input type="file" id="audio-workflow-file" accept=".json">
<button type="button" class="btn btn-sm" onclick="uploadWorkflow('audio')">Upload</button>
{% if workflows.audio %}
<button type="button" class="btn btn-sm" onclick="viewWorkflow('audio')">View JSON</button>
<button type="button" class="btn btn-danger btn-sm" onclick="deleteWorkflow('audio')">Delete</button>
{% endif %}
</div>
</div>
<div class="node-mappings">
<h4>Node ID Mappings</h4>
<div class="mapping-grid">
<div class="form-group">
<label for="audio_prompt_node">Prompt Node ID</label>
<input type="text" id="audio_prompt_node" name="audio_prompt_node"
value="{{ config.get('audio_prompt_node', '') }}" placeholder="e.g., 6">
</div>
<div class="form-group">
<label for="audio_seed_node">Seed Node ID</label>
<input type="text" id="audio_seed_node" name="audio_seed_node"
value="{{ config.get('audio_seed_node', '') }}" placeholder="e.g., 3">
</div>
<div class="form-group">
<label for="audio_duration_node">Duration Node ID</label>
<input type="text" id="audio_duration_node" name="audio_duration_node"
value="{{ config.get('audio_duration_node', '') }}" placeholder="e.g., 5">
</div>
<div class="form-group">
<label for="audio_output_node">Output Node ID</label>
<input type="text" id="audio_output_node" name="audio_output_node"
value="{{ config.get('audio_output_node', '') }}" placeholder="e.g., 9">
</div>
</div>
<div class="form-group">
<label for="audio_default_duration">Default Duration (seconds)</label>
<input type="number" id="audio_default_duration" name="audio_default_duration"
value="{{ config.get('audio_default_duration', 10) }}" step="0.5" placeholder="10">
</div>
</div>
<button type="submit" class="btn btn-primary">Save Audio Settings</button>
</form>
</div>
</section>
<div id="toast" class="toast hidden"></div>
<div id="modal" class="modal hidden">
<div class="modal-content">
<div class="modal-header">
<h3>Workflow JSON</h3>
<button class="modal-close" onclick="closeModal()">&times;</button>
</div>
<pre id="modal-json"></pre>
</div>
</div>
</main>
<script>
// Tab switching
document.querySelectorAll('.tab-btn').forEach(btn => {
btn.addEventListener('click', () => {
document.querySelectorAll('.tab-btn').forEach(b => b.classList.remove('active'));
document.querySelectorAll('.tab-content').forEach(c => c.classList.remove('active'));
btn.classList.add('active');
document.getElementById(btn.dataset.tab + '-tab').classList.add('active');
});
});
// Connection form
document.getElementById('connection-form').addEventListener('submit', async (e) => {
e.preventDefault();
const formData = new FormData(e.target);
await saveConfig({ comfyui_host: formData.get('comfyui_host') });
testConnection();
});
// Workflow forms
['image', 'video', 'audio'].forEach(type => {
document.getElementById(`${type}-workflow-form`).addEventListener('submit', async (e) => {
e.preventDefault();
const formData = new FormData(e.target);
const data = {};
formData.forEach((value, key) => data[key] = value);
await saveConfig(data);
});
});
// Upload workflow
async function uploadWorkflow(type) {
const fileInput = document.getElementById(`${type}-workflow-file`);
const file = fileInput.files[0];
if (!file) {
showToast('Please select a file', 'error');
return;
}
const formData = new FormData();
formData.append('file', file);
formData.append('workflow_type', type);
try {
const response = await fetch('/{{ settings.admin_path }}/comfyui/upload', {
method: 'POST',
body: formData
});
const result = await response.json();
if (result.status === 'success') {
showToast(`Workflow uploaded successfully`, 'success');
setTimeout(() => location.reload(), 1000);
} else {
showToast('Upload failed: ' + (result.detail || 'Unknown error'), 'error');
}
} catch (error) {
showToast('Upload failed', 'error');
}
}
// View workflow
async function viewWorkflow(type) {
try {
const response = await fetch(`/{{ settings.admin_path }}/comfyui/${type}`);
const data = await response.json();
document.getElementById('modal-json').textContent = JSON.stringify(data, null, 2);
document.getElementById('modal').classList.remove('hidden');
} catch (error) {
showToast('Failed to load workflow', 'error');
}
}
// Delete workflow
async function deleteWorkflow(type) {
if (!confirm('Delete this workflow?')) return;
try {
const response = await fetch(`/{{ settings.admin_path }}/comfyui/${type}`, {
method: 'DELETE'
});
const result = await response.json();
if (result.status === 'success') {
showToast('Workflow deleted', 'success');
location.reload();
}
} catch (error) {
showToast('Delete failed', 'error');
}
}
// Save config
async function saveConfig(data) {
try {
const response = await fetch('/{{ settings.admin_path }}/endpoints', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
});
const result = await response.json();
if (result.status === 'success') {
showToast('Settings saved', 'success');
} else {
showToast('Failed to save', 'error');
}
} catch (error) {
showToast('Failed to save', 'error');
}
}
// Test connection
async function testConnection() {
const statusEl = document.getElementById('connection-status');
statusEl.textContent = 'Testing...';
statusEl.className = 'status-indicator checking';
try {
const response = await fetch('/{{ settings.admin_path }}/status');
const data = await response.json();
if (data.comfyui === 'connected') {
statusEl.textContent = 'Connected';
statusEl.className = 'status-indicator connected';
} else {
statusEl.textContent = 'Disconnected';
statusEl.className = 'status-indicator disconnected';
}
} catch (error) {
statusEl.textContent = 'Error';
statusEl.className = 'status-indicator disconnected';
}
}
// Modal
function closeModal() {
document.getElementById('modal').classList.add('hidden');
}
document.getElementById('modal').addEventListener('click', (e) => {
if (e.target.id === 'modal') closeModal();
});
// Toast
function showToast(message, type) {
const toast = document.getElementById('toast');
toast.textContent = message;
toast.className = 'toast ' + type;
setTimeout(() => toast.classList.add('hidden'), 3000);
}
// Initial connection test
testConnection();
</script>
</body>
</html>