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 + + + +
+
+

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

+ +
+ +
+ ${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), ])