From abf2f0e6014c9d5399b9e11c38a52ca5a867713f Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E8=AF=BA=E6=96=AF=E8=B4=B9=E6=8B=89=E5=9B=BE?=
<1132505822@qq.com>
Date: Sun, 12 Apr 2026 17:16:34 +0800
Subject: [PATCH] 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
---
research_web/css/research.css | 110 ++++++++++++++++++++++
research_web/index.html | 26 ++++++
research_web/js/api_client.js | 121 ++++++++++++++++++++++++
research_web/js/app.js | 161 ++++++++++++++++++++++++++++++++
research_web/js/nodes/.gitkeep | 0
research_web/js/panels/.gitkeep | 0
server.py | 5 +
7 files changed, 423 insertions(+)
create mode 100644 research_web/css/research.css
create mode 100644 research_web/index.html
create mode 100644 research_web/js/api_client.js
create mode 100644 research_web/js/app.js
create mode 100644 research_web/js/nodes/.gitkeep
create mode 100644 research_web/js/panels/.gitkeep
diff --git a/research_web/css/research.css b/research_web/css/research.css
new file mode 100644
index 000000000..c41047aa1
--- /dev/null
+++ b/research_web/css/research.css
@@ -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; }
diff --git a/research_web/index.html b/research_web/index.html
new file mode 100644
index 000000000..a2439e814
--- /dev/null
+++ b/research_web/index.html
@@ -0,0 +1,26 @@
+
+
+
+
+
+ Research Workbench
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/research_web/js/api_client.js b/research_web/js/api_client.js
new file mode 100644
index 000000000..295b7aa35
--- /dev/null
+++ b/research_web/js/api_client.js
@@ -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();
+}
diff --git a/research_web/js/app.js b/research_web/js/app.js
new file mode 100644
index 000000000..5e7a7db3e
--- /dev/null
+++ b/research_web/js/app.js
@@ -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 = `Failed to load: ${e.message}
`;
+ }
+ } else if (currentPanel === "projects") {
+ try {
+ const projects = await api.listProjects();
+ mainContent.innerHTML = "";
+ renderProjectPanel(mainContent, { projects }, navigateTo);
+ } catch (e) {
+ mainContent.innerHTML = `Failed to load: ${e.message}
`;
+ }
+ } 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 = `
+
+
+
+
${projectCount}
+
Total Projects
+
+
+
${activeProjects}
+
Active
+
+
+
${papers.length}
+
Papers in Library
+
+
+
${feedItems.length}
+
Today's Feed
+
+
+
+
+ Today Feed
+ ${feedItems.length === 0 ? 'No papers in feed yet.
' :
+ feedItems.map(item => `
+
+
${item.title || "Untitled"}
+
${item.authors_text || ""} ยท ${item.published_at || ""}
+
${item.abstract || ""}
+
+
+
+
+ `).join("")}
+
+
+
+ Project Focus
+ ${projects.length === 0 ? 'No projects yet.
' :
+ projects.slice(0, 5).map(p => `
+
+
${p.title}
+
${p.goal || "No goal set"}
+
${p.status}
+
+ `).join("")}
+
+
+
+ Quick Entry
+
+
+
+ `;
+
+ 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 = `
+
+
+
+
+ ${projects.length === 0 ? '
No projects. Create your first project.
' :
+ projects.map(p => `
+
+
+
${p.title}
+
${p.goal || "No goal set"}
+
${p.status}
+
+
+
+
+
+ `).join("")}
+
+
+ `;
+
+ 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();
diff --git a/research_web/js/nodes/.gitkeep b/research_web/js/nodes/.gitkeep
new file mode 100644
index 000000000..e69de29bb
diff --git a/research_web/js/panels/.gitkeep b/research_web/js/panels/.gitkeep
new file mode 100644
index 000000000..e69de29bb
diff --git a/server.py b/server.py
index ef152540e..4114826c8 100644
--- a/server.py
+++ b/server.py
@@ -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),
])