mirror of
https://github.com/comfyanonymous/ComfyUI.git
synced 2026-04-15 04:52:31 +08:00
feat: add web frontend shells (Home panel, Project panel, API client)
- Add research_web/index.html as entry point with navigation - Add research_web/js/api_client.js with all research API endpoints - Add research_web/js/app.js with Home and Project panel renderers - Add research_web/css/research.css with dark theme styling - Add server.py static route for /research endpoint - Add placeholder .gitkeep files for empty subdirectories
This commit is contained in:
parent
be9241aa0f
commit
abf2f0e601
110
research_web/css/research.css
Normal file
110
research_web/css/research.css
Normal file
@ -0,0 +1,110 @@
|
||||
/* Research Workbench Styles */
|
||||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
||||
background: #1e1e1e;
|
||||
color: #d4d4d4;
|
||||
}
|
||||
|
||||
#app { min-height: 100vh; display: flex; flex-direction: column; }
|
||||
|
||||
.app-header {
|
||||
background: #252526;
|
||||
border-bottom: 1px solid #3c3c3c;
|
||||
padding: 0.5rem 1rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 2rem;
|
||||
}
|
||||
|
||||
.app-header h1 { font-size: 1.2rem; color: #fff; }
|
||||
|
||||
.app-nav { display: flex; gap: 0.25rem; }
|
||||
|
||||
.nav-btn {
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: #d4d4d4;
|
||||
padding: 0.5rem 1rem;
|
||||
cursor: pointer;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.nav-btn:hover { background: #3c3c3c; }
|
||||
.nav-btn.active { background: #37373d; color: #fff; }
|
||||
|
||||
#main-content { flex: 1; padding: 1rem; overflow: auto; }
|
||||
|
||||
.home-panel, .project-panel { max-width: 1200px; margin: 0 auto; }
|
||||
|
||||
/* Top Summary */
|
||||
.top-summary { display: grid; grid-template-columns: repeat(4, 1fr); gap: 1rem; margin-bottom: 2rem; }
|
||||
.summary-card { background: #252526; border-radius: 8px; padding: 1.5rem; text-align: center; }
|
||||
.summary-card h3 { font-size: 2rem; color: #fff; }
|
||||
.summary-card p { color: #858585; font-size: 0.85rem; }
|
||||
|
||||
/* Sections */
|
||||
.today-feed, .project-focus, .quick-entry { margin-bottom: 2rem; }
|
||||
.today-feed h2, .project-focus h2, .quick-entry h2 { margin-bottom: 1rem; font-size: 1.1rem; color: #fff; }
|
||||
|
||||
/* Feed Cards */
|
||||
.feed-card {
|
||||
background: #252526;
|
||||
border-radius: 6px;
|
||||
padding: 1rem;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
.feed-card h4 { color: #fff; margin-bottom: 0.25rem; }
|
||||
.feed-meta { color: #858585; font-size: 0.8rem; margin-bottom: 0.5rem; }
|
||||
.feed-abstract { font-size: 0.9rem; color: #ccc; margin-bottom: 0.75rem; }
|
||||
.feed-actions { display: flex; gap: 0.5rem; }
|
||||
|
||||
.btn-save {
|
||||
background: #37373d; border: none; color: #d4d4d4;
|
||||
padding: 0.4rem 0.8rem; border-radius: 4px; cursor: pointer; font-size: 0.85rem;
|
||||
}
|
||||
.btn-save:hover { background: #4f4f4f; }
|
||||
|
||||
/* Project Cards */
|
||||
.project-card {
|
||||
background: #252526; border-radius: 6px; padding: 1rem;
|
||||
margin-bottom: 0.75rem; cursor: pointer;
|
||||
}
|
||||
.project-card:hover { background: #2d2d30; }
|
||||
.project-card h4 { color: #fff; }
|
||||
|
||||
.status-badge {
|
||||
display: inline-block; font-size: 0.75rem; padding: 0.2rem 0.5rem;
|
||||
border-radius: 3px; margin-top: 0.5rem;
|
||||
}
|
||||
.status-badge.active { background: #2ea043; color: #fff; }
|
||||
.status-badge.paused { background: #d29922; color: #000; }
|
||||
.status-badge.completed { background: #388bfd; color: #fff; }
|
||||
|
||||
/* Buttons */
|
||||
.btn-primary {
|
||||
background: #0e639c; border: none; color: #fff;
|
||||
padding: 0.5rem 1rem; border-radius: 4px; cursor: pointer;
|
||||
}
|
||||
.btn-primary:hover { background: #1177bb; }
|
||||
|
||||
.project-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 1.5rem; }
|
||||
.project-header h2 { color: #fff; }
|
||||
|
||||
.project-item {
|
||||
background: #252526; border-radius: 6px; padding: 1rem;
|
||||
margin-bottom: 0.75rem; display: flex; justify-content: space-between; align-items: center;
|
||||
}
|
||||
.project-item h3 { color: #fff; font-size: 1rem; }
|
||||
.project-item p { color: #858585; font-size: 0.85rem; }
|
||||
.project-actions { display: flex; gap: 0.5rem; }
|
||||
|
||||
.btn-open-canvas {
|
||||
background: #37373d; border: none; color: #d4d4d4;
|
||||
padding: 0.4rem 0.8rem; border-radius: 4px; cursor: pointer;
|
||||
}
|
||||
.btn-open-canvas:hover { background: #4f4f4f; }
|
||||
|
||||
.empty-state { color: #858585; font-style: italic; padding: 1rem; }
|
||||
.error { color: #f48771; padding: 1rem; }
|
||||
26
research_web/index.html
Normal file
26
research_web/index.html
Normal file
@ -0,0 +1,26 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Research Workbench</title>
|
||||
<link rel="stylesheet" href="./css/research.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="app">
|
||||
<header class="app-header">
|
||||
<h1>Research Workbench</h1>
|
||||
<nav class="app-nav">
|
||||
<button id="nav-home" class="nav-btn active">Home</button>
|
||||
<button id="nav-projects" class="nav-btn">Projects</button>
|
||||
<button id="nav-assets" class="nav-btn">Assets</button>
|
||||
<button id="nav-canvas" class="nav-btn">Canvas</button>
|
||||
</nav>
|
||||
</header>
|
||||
<main id="main-content">
|
||||
<!-- Panels rendered here -->
|
||||
</main>
|
||||
</div>
|
||||
<script type="module" src="./js/app.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
121
research_web/js/api_client.js
Normal file
121
research_web/js/api_client.js
Normal file
@ -0,0 +1,121 @@
|
||||
/** Research API client - talks to ComfyUI's built-in research routes via aiohttp. */
|
||||
|
||||
const API_BASE = "/research";
|
||||
|
||||
export async function listProjects() {
|
||||
const res = await fetch(`${API_BASE}/projects/`);
|
||||
return res.json();
|
||||
}
|
||||
|
||||
export async function createProject(data) {
|
||||
const res = await fetch(`${API_BASE}/projects/`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
return res.json();
|
||||
}
|
||||
|
||||
export async function getProject(projectId) {
|
||||
const res = await fetch(`${API_BASE}/projects/${projectId}`);
|
||||
return res.json();
|
||||
}
|
||||
|
||||
export async function updateProject(projectId, data) {
|
||||
const res = await fetch(`${API_BASE}/projects/${projectId}`, {
|
||||
method: "PATCH",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
return res.json();
|
||||
}
|
||||
|
||||
export async function listIntents(projectId) {
|
||||
const res = await fetch(`${API_BASE}/projects/${projectId}/intents`);
|
||||
return res.json();
|
||||
}
|
||||
|
||||
export async function createIntent(projectId, data) {
|
||||
const res = await fetch(`${API_BASE}/projects/${projectId}/intents`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
return res.json();
|
||||
}
|
||||
|
||||
export async function updateIntent(intentId, data) {
|
||||
const res = await fetch(`${API_BASE}/projects/intents/${intentId}`, {
|
||||
method: "PATCH",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
return res.json();
|
||||
}
|
||||
|
||||
export async function listPapers(params = {}) {
|
||||
const qs = new URLSearchParams(params).toString();
|
||||
const res = await fetch(`${API_BASE}/papers/${qs ? "?" + qs : ""}`);
|
||||
return res.json();
|
||||
}
|
||||
|
||||
export async function createPaper(data) {
|
||||
const res = await fetch(`${API_BASE}/papers/`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
return res.json();
|
||||
}
|
||||
|
||||
export async function updatePaper(paperId, data) {
|
||||
const res = await fetch(`${API_BASE}/papers/${paperId}`, {
|
||||
method: "PATCH",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
return res.json();
|
||||
}
|
||||
|
||||
export async function listClaims(params = {}) {
|
||||
const qs = new URLSearchParams(params).toString();
|
||||
const res = await fetch(`${API_BASE}/claims/${qs ? "?" + qs : ""}`);
|
||||
return res.json();
|
||||
}
|
||||
|
||||
export async function createClaim(data) {
|
||||
const res = await fetch(`${API_BASE}/claims/`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
return res.json();
|
||||
}
|
||||
|
||||
export async function updateClaim(claimId, data) {
|
||||
const res = await fetch(`${API_BASE}/claims/${claimId}`, {
|
||||
method: "PATCH",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
return res.json();
|
||||
}
|
||||
|
||||
export async function getTodayFeed() {
|
||||
const res = await fetch(`${API_BASE}/feed/today`);
|
||||
return res.json();
|
||||
}
|
||||
|
||||
export async function listSources() {
|
||||
const res = await fetch(`${API_BASE}/sources/`);
|
||||
return res.json();
|
||||
}
|
||||
|
||||
export async function createSource(data) {
|
||||
const res = await fetch(`${API_BASE}/sources/`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
return res.json();
|
||||
}
|
||||
161
research_web/js/app.js
Normal file
161
research_web/js/app.js
Normal file
@ -0,0 +1,161 @@
|
||||
/** Main Research Workbench app - Home and Project Workspace panels. */
|
||||
import * as api from "./api_client.js";
|
||||
|
||||
const mainContent = document.getElementById("main-content");
|
||||
const navButtons = document.querySelectorAll(".nav-btn");
|
||||
|
||||
let currentPanel = "home";
|
||||
let currentProjectId = null;
|
||||
|
||||
function navigateTo(panel, projectId = null) {
|
||||
currentPanel = panel;
|
||||
currentProjectId = projectId;
|
||||
navButtons.forEach(btn => btn.classList.remove("active"));
|
||||
document.getElementById(`nav-${panel}`)?.classList.add("active");
|
||||
render();
|
||||
}
|
||||
|
||||
async function render() {
|
||||
if (currentPanel === "home") {
|
||||
try {
|
||||
const [projects, feedItems, papers] = await Promise.all([
|
||||
api.listProjects(),
|
||||
api.getTodayFeed(),
|
||||
api.listPapers({ library_status: "library" }),
|
||||
]);
|
||||
mainContent.innerHTML = "";
|
||||
renderHomePanel(mainContent, { projects, feedItems, papers }, navigateTo);
|
||||
} catch (e) {
|
||||
mainContent.innerHTML = `<p class="error">Failed to load: ${e.message}</p>`;
|
||||
}
|
||||
} else if (currentPanel === "projects") {
|
||||
try {
|
||||
const projects = await api.listProjects();
|
||||
mainContent.innerHTML = "";
|
||||
renderProjectPanel(mainContent, { projects }, navigateTo);
|
||||
} catch (e) {
|
||||
mainContent.innerHTML = `<p class="error">Failed to load: ${e.message}</p>`;
|
||||
}
|
||||
} else if (currentPanel === "canvas") {
|
||||
window.location.href = "/";
|
||||
}
|
||||
}
|
||||
|
||||
function renderHomePanel(container, { projects, feedItems, papers }, navigateTo) {
|
||||
const projectCount = projects.length || 0;
|
||||
const activeProjects = projects.filter(p => p.status === "active").length;
|
||||
|
||||
container.innerHTML = `
|
||||
<div class="home-panel">
|
||||
<section class="top-summary">
|
||||
<div class="summary-card">
|
||||
<h3>${projectCount}</h3>
|
||||
<p>Total Projects</p>
|
||||
</div>
|
||||
<div class="summary-card">
|
||||
<h3>${activeProjects}</h3>
|
||||
<p>Active</p>
|
||||
</div>
|
||||
<div class="summary-card">
|
||||
<h3>${papers.length}</h3>
|
||||
<p>Papers in Library</p>
|
||||
</div>
|
||||
<div class="summary-card">
|
||||
<h3>${feedItems.length}</h3>
|
||||
<p>Today's Feed</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="today-feed">
|
||||
<h2>Today Feed</h2>
|
||||
${feedItems.length === 0 ? '<p class="empty-state">No papers in feed yet.</p>' :
|
||||
feedItems.map(item => `
|
||||
<div class="feed-card">
|
||||
<h4>${item.title || "Untitled"}</h4>
|
||||
<p class="feed-meta">${item.authors_text || ""} · ${item.published_at || ""}</p>
|
||||
<p class="feed-abstract">${item.abstract || ""}</p>
|
||||
<div class="feed-actions">
|
||||
<button class="btn-save" data-id="${item.id}">Save to Library</button>
|
||||
</div>
|
||||
</div>
|
||||
`).join("")}
|
||||
</section>
|
||||
|
||||
<section class="project-focus">
|
||||
<h2>Project Focus</h2>
|
||||
${projects.length === 0 ? '<p class="empty-state">No projects yet.</p>' :
|
||||
projects.slice(0, 5).map(p => `
|
||||
<div class="project-card" data-id="${p.id}">
|
||||
<h4>${p.title}</h4>
|
||||
<p>${p.goal || "No goal set"}</p>
|
||||
<span class="status-badge ${p.status}">${p.status}</span>
|
||||
</div>
|
||||
`).join("")}
|
||||
</section>
|
||||
|
||||
<section class="quick-entry">
|
||||
<h2>Quick Entry</h2>
|
||||
<button id="btn-new-project" class="btn-primary">+ New Project</button>
|
||||
</section>
|
||||
</div>
|
||||
`;
|
||||
|
||||
document.getElementById("btn-new-project")?.addEventListener("click", async () => {
|
||||
const title = prompt("Project title:");
|
||||
if (title) {
|
||||
await api.createProject({ title, goal: "" });
|
||||
navigateTo("projects");
|
||||
}
|
||||
});
|
||||
|
||||
container.querySelectorAll(".project-card").forEach(card => {
|
||||
card.addEventListener("click", () => {
|
||||
navigateTo("projects");
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function renderProjectPanel(container, { projects }, navigateTo) {
|
||||
container.innerHTML = `
|
||||
<div class="project-panel">
|
||||
<div class="project-header">
|
||||
<h2>Projects</h2>
|
||||
<button id="btn-create-project" class="btn-primary">+ New Project</button>
|
||||
</div>
|
||||
|
||||
<div class="project-list">
|
||||
${projects.length === 0 ? '<p class="empty-state">No projects. Create your first project.</p>' :
|
||||
projects.map(p => `
|
||||
<div class="project-item" data-id="${p.id}">
|
||||
<div class="project-info">
|
||||
<h3>${p.title}</h3>
|
||||
<p>${p.goal || "No goal set"}</p>
|
||||
<span class="status-badge ${p.status}">${p.status}</span>
|
||||
</div>
|
||||
<div class="project-actions">
|
||||
<button class="btn-open-canvas" data-id="${p.id}">Open Canvas</button>
|
||||
</div>
|
||||
</div>
|
||||
`).join("")}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
document.getElementById("btn-create-project")?.addEventListener("click", async () => {
|
||||
const title = prompt("Project title:");
|
||||
if (title) {
|
||||
await api.createProject({ title, goal: "" });
|
||||
const projects = await api.listProjects();
|
||||
renderProjectPanel(container, { projects }, navigateTo);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Wire up navigation
|
||||
document.getElementById("nav-home").addEventListener("click", () => navigateTo("home"));
|
||||
document.getElementById("nav-projects").addEventListener("click", () => navigateTo("projects"));
|
||||
document.getElementById("nav-assets").addEventListener("click", () => navigateTo("assets"));
|
||||
document.getElementById("nav-canvas").addEventListener("click", () => navigateTo("canvas"));
|
||||
|
||||
// Initial render
|
||||
render();
|
||||
0
research_web/js/nodes/.gitkeep
Normal file
0
research_web/js/nodes/.gitkeep
Normal file
0
research_web/js/panels/.gitkeep
Normal file
0
research_web/js/panels/.gitkeep
Normal file
@ -1104,6 +1104,11 @@ class PromptServer():
|
||||
web.static('/docs', embedded_docs_path)
|
||||
])
|
||||
|
||||
# Serve research workbench web frontend
|
||||
research_web_path = os.path.join(os.path.dirname(__file__), "research_web")
|
||||
if os.path.exists(research_web_path):
|
||||
self.app.add_routes([web.static('/research', research_web_path)])
|
||||
|
||||
self.app.add_routes([
|
||||
web.static('/', self.web_root),
|
||||
])
|
||||
|
||||
Loading…
Reference in New Issue
Block a user