mirror of
https://github.com/rookiestar28/ComfyUI-OpenClaw.git
synced 2026-08-14 08:52:45 +00:00
319 lines
11 KiB
JavaScript
319 lines
11 KiB
JavaScript
/**
|
|
* F7: OpenClaw UI Shell
|
|
* Manages the main sidebar layout: Header, Tabs, Content.
|
|
*/
|
|
import { tabManager } from "./openclaw_tabs.js";
|
|
import { ErrorBoundary } from "./ErrorBoundary.js";
|
|
import { openclawApi } from "./openclaw_api.js";
|
|
import {
|
|
applyLegacyClassAliases,
|
|
normalizeLegacyClassNames,
|
|
} from "./openclaw_utils.js";
|
|
import { OpenClawActions } from "./openclaw_actions.js";
|
|
import { QueueMonitor } from "./openclaw_queue_monitor.js";
|
|
import { OpenClawNotificationCenter } from "./openclaw_notification_center.js";
|
|
import { OpenClawBannerManager } from "./openclaw_banner_manager.js";
|
|
|
|
export class OpenClawUI {
|
|
constructor() {
|
|
this.container = null;
|
|
this.boundary = new ErrorBoundary("OpenClawUI");
|
|
this.floating = {
|
|
panel: null,
|
|
content: null,
|
|
};
|
|
this.notificationCenter = new OpenClawNotificationCenter({
|
|
onAction: (action) => this.handleAction(action),
|
|
});
|
|
this.bannerManager = new OpenClawBannerManager({
|
|
onAction: (action) => this.handleAction(action),
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Mount the UI into a provided container (sidebar render target).
|
|
*/
|
|
mount(container) {
|
|
this.container = container;
|
|
// CRITICAL: Must run before render to prevent first-paint clipping in Splitter sidebar host.
|
|
this._enforceSidebarMinWidth(container);
|
|
this.boundary.run(container, () => this._render(container));
|
|
}
|
|
|
|
_enforceSidebarMinWidth(container) {
|
|
// IMPORTANT: Keep this value aligned with CSS .openclaw-sidebar-container min-width.
|
|
const minWidthPx = 560;
|
|
|
|
const applyMinWidth = () => {
|
|
// CRITICAL: Sidebar width is controlled by ComfyUI SplitterPanel (.side-bar-panel),
|
|
// not by our inner root container. If we only set inner min-width, content gets clipped.
|
|
const sidePanel = container.closest(".side-bar-panel");
|
|
const splitterPanel = sidePanel || container.closest(".p-splitterpanel");
|
|
if (splitterPanel) {
|
|
splitterPanel.style.minWidth = `${minWidthPx}px`;
|
|
}
|
|
|
|
const sidebarContent = container.closest(".sidebar-content-container");
|
|
if (sidebarContent) {
|
|
sidebarContent.style.minWidth = `${minWidthPx}px`;
|
|
}
|
|
|
|
container.style.minWidth = `${minWidthPx}px`;
|
|
};
|
|
|
|
// Run once now, then again after mount/paint to handle late-attached sidebar wrappers.
|
|
applyMinWidth();
|
|
if (typeof requestAnimationFrame === "function") {
|
|
requestAnimationFrame(applyMinWidth);
|
|
} else {
|
|
setTimeout(applyMinWidth, 0);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Legacy fallback: toggle a floating panel (must not touch document.body directly).
|
|
*/
|
|
toggleFloatingPanel() {
|
|
if (!this.floating.panel) {
|
|
const panel = document.createElement("div");
|
|
panel.className = "openclaw-floating-panel";
|
|
|
|
const close = document.createElement("button");
|
|
close.className = "openclaw-floating-close";
|
|
close.textContent = "\u00D7";
|
|
close.title = "Close";
|
|
close.addEventListener("click", () => {
|
|
panel.classList.remove("visible");
|
|
});
|
|
|
|
const content = document.createElement("div");
|
|
content.className = "openclaw-floating-content";
|
|
|
|
panel.appendChild(close);
|
|
panel.appendChild(content);
|
|
document.body.appendChild(panel);
|
|
|
|
this.floating.panel = panel;
|
|
this.floating.content = content;
|
|
}
|
|
|
|
const panel = this.floating.panel;
|
|
const content = this.floating.content;
|
|
const isVisible = panel.classList.contains("visible");
|
|
|
|
if (isVisible) {
|
|
panel.classList.remove("visible");
|
|
return;
|
|
}
|
|
|
|
panel.classList.add("visible");
|
|
this.mount(content);
|
|
}
|
|
|
|
_render(container) {
|
|
container.innerHTML = "";
|
|
container.className = "openclaw-sidebar-container";
|
|
|
|
const header = document.createElement("div");
|
|
header.className = "openclaw-header";
|
|
|
|
const statusDot = document.createElement("div");
|
|
statusDot.className = "openclaw-status-dot ok";
|
|
statusDot.title = "System Status";
|
|
this.statusDot = statusDot;
|
|
|
|
const title = document.createElement("div");
|
|
title.className = "openclaw-title";
|
|
title.textContent = "OpenClaw";
|
|
|
|
const badges = document.createElement("div");
|
|
badges.className = "openclaw-badges";
|
|
|
|
const versionSpan = document.createElement("span");
|
|
versionSpan.className = "openclaw-version";
|
|
versionSpan.textContent = "v...";
|
|
|
|
const repoLink = document.createElement("a");
|
|
repoLink.href = "https://github.com/rookiestar28/ComfyUI-OpenClaw";
|
|
repoLink.target = "_blank";
|
|
repoLink.className = "openclaw-repo-link";
|
|
repoLink.title = "View on GitHub";
|
|
repoLink.textContent = "View on GitHub";
|
|
|
|
badges.appendChild(versionSpan);
|
|
badges.appendChild(repoLink);
|
|
badges.appendChild(this.notificationCenter.buildToggle());
|
|
|
|
openclawApi.getHealth().then((res) => {
|
|
if (res.ok && res.data) {
|
|
const data = res.data;
|
|
if (data.pack) {
|
|
versionSpan.textContent = `v${data.pack.version}`;
|
|
}
|
|
|
|
const cpMode = data?.control_plane?.mode || data?.deployment_profile || "local";
|
|
const modeBadge = document.createElement("span");
|
|
modeBadge.className = `openclaw-mode-badge openclaw-mode-${cpMode}`;
|
|
modeBadge.textContent = cpMode.toUpperCase();
|
|
modeBadge.title = `Control plane: ${cpMode}`;
|
|
modeBadge.style.cssText = `
|
|
font-size: 10px; padding: 1px 6px; border-radius: 4px;
|
|
font-weight: 600; margin-left: 6px; letter-spacing: 0.5px;
|
|
`;
|
|
if (cpMode === "split") {
|
|
modeBadge.style.background = "#f59e0b";
|
|
modeBadge.style.color = "#1a1a2e";
|
|
} else if (cpMode === "hardened") {
|
|
modeBadge.style.background = "#ef4444";
|
|
modeBadge.style.color = "#fff";
|
|
} else {
|
|
modeBadge.style.background = "#22c55e";
|
|
modeBadge.style.color = "#1a1a2e";
|
|
}
|
|
badges.appendChild(modeBadge);
|
|
this._controlPlaneMode = cpMode;
|
|
|
|
this.checkExposure(data?.access_policy);
|
|
|
|
const obs = data.stats?.observability;
|
|
if (obs && obs.total_dropped > 0) {
|
|
this.showBanner(
|
|
"warning",
|
|
`\u26A0\uFE0F High load: ${obs.total_dropped} observability events dropped (Queue full). logs/traces might be incomplete.`
|
|
);
|
|
}
|
|
} else {
|
|
versionSpan.textContent = "v?.?.?";
|
|
}
|
|
}).catch(() => {
|
|
versionSpan.textContent = "v?.?.?";
|
|
});
|
|
|
|
header.appendChild(statusDot);
|
|
header.appendChild(title);
|
|
header.appendChild(badges);
|
|
container.appendChild(header);
|
|
container.appendChild(this.notificationCenter.buildPanel());
|
|
|
|
const tabBar = document.createElement("div");
|
|
tabBar.className = "openclaw-tabs";
|
|
this.tabBar = tabBar;
|
|
container.appendChild(tabBar);
|
|
|
|
const contentArea = document.createElement("div");
|
|
contentArea.className = "openclaw-content";
|
|
this.contentArea = contentArea;
|
|
container.appendChild(contentArea);
|
|
|
|
tabManager.init(tabBar, contentArea);
|
|
normalizeLegacyClassNames(container);
|
|
applyLegacyClassAliases(container);
|
|
this.bannerManager.bind(container, (action) => this.handleAction(action));
|
|
this.notificationCenter.render();
|
|
}
|
|
|
|
toggleNotifications() {
|
|
this.notificationCenter.toggle();
|
|
}
|
|
|
|
/**
|
|
* S15: Check if OpenClaw is exposed remotely and warn user.
|
|
*/
|
|
checkExposure(policy) {
|
|
if (!policy) return;
|
|
|
|
const isLocal = ["localhost", "127.0.0.1", "::1"].includes(window.location.hostname);
|
|
if (!isLocal) {
|
|
const isProtected = policy.observability === "token" && policy.token_configured;
|
|
if (!isProtected) {
|
|
this.showBanner(
|
|
"warning",
|
|
"\u26A0\uFE0F Remote access detected; logs/config are protected unless you explicitly enable token-based access."
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* F49: Display a banner with id, severity, ttl, and optional action.
|
|
* @param {Object} banner - { id, severity, message, source, ttl_ms, dismissible, action }
|
|
*/
|
|
showBanner(banner) {
|
|
this.bannerManager.showBanner(...arguments);
|
|
}
|
|
|
|
handleAction(action) {
|
|
if (!action) return;
|
|
|
|
switch (action.type) {
|
|
case "url":
|
|
window.open(action.payload, "_blank");
|
|
break;
|
|
case "tab":
|
|
tabManager.activateTab(action.payload);
|
|
break;
|
|
case "action":
|
|
if (openclawActions && openclawActions.dispatch) {
|
|
openclawActions.dispatch(action.payload);
|
|
} else {
|
|
console.log("Action triggered:", action.payload);
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* F51: Glassmorphism Confirmation Modal.
|
|
* @param {Object} options - { title, message, fatal, onConfirm }
|
|
*/
|
|
showConfirm({ title, message, fatal = false, onConfirm }) {
|
|
const overlay = document.createElement("div");
|
|
overlay.className = "openclaw-modal-overlay";
|
|
|
|
const modal = document.createElement("div");
|
|
modal.className = `openclaw-modal ${fatal ? "fatal" : ""}`;
|
|
|
|
const h3 = document.createElement("h3");
|
|
h3.textContent = title || "Confirm Action";
|
|
|
|
const p = document.createElement("p");
|
|
p.textContent = message || "Are you sure?";
|
|
|
|
const buttons = document.createElement("div");
|
|
buttons.className = "openclaw-modal-buttons";
|
|
|
|
const cancelBtn = document.createElement("button");
|
|
cancelBtn.className = "openclaw-btn secondary";
|
|
cancelBtn.textContent = "Cancel";
|
|
cancelBtn.onclick = () => overlay.remove();
|
|
|
|
const confirmBtn = document.createElement("button");
|
|
confirmBtn.className = `openclaw-btn ${fatal ? "danger" : "primary"}`;
|
|
confirmBtn.textContent = "Confirm";
|
|
confirmBtn.onclick = () => {
|
|
overlay.remove();
|
|
if (onConfirm) onConfirm();
|
|
};
|
|
|
|
buttons.appendChild(cancelBtn);
|
|
buttons.appendChild(confirmBtn);
|
|
|
|
modal.appendChild(h3);
|
|
modal.appendChild(p);
|
|
modal.appendChild(buttons);
|
|
overlay.appendChild(modal);
|
|
|
|
this.container.appendChild(overlay);
|
|
normalizeLegacyClassNames(overlay);
|
|
applyLegacyClassAliases(overlay);
|
|
}
|
|
}
|
|
|
|
export const openclawUI = new OpenClawUI();
|
|
export const openclawActions = new OpenClawActions(openclawUI);
|
|
export { OpenClawActions } from "./openclaw_actions.js";
|
|
export { QueueMonitor } from "./openclaw_queue_monitor.js";
|
|
|
|
const monitor = new QueueMonitor(openclawUI);
|
|
monitor.start();
|