mirror of
https://github.com/rookiestar28/ComfyUI-OpenClaw.git
synced 2026-08-14 00:48:07 +00:00
F63/R138/R140: add frontend quality baseline and coverage
This commit is contained in:
@@ -91,6 +91,17 @@ Deployment profiles and hardening checklists:
|
||||
|
||||
<details>
|
||||
|
||||
<summary><strong>Frontend quality baseline for Library and Approvals surfaces</strong></summary>
|
||||
|
||||
- Canonicalized active frontend styling ownership around `openclaw-*`, including shell/tab-manager cleanup and a deterministic split of `web/openclaw.css` into core and legacy-alias modules.
|
||||
- Added a frontend unit-test lane with Vitest + jsdom plus baseline coverage for shared UI helpers and extracted Library tab state logic.
|
||||
- Expanded Playwright coverage for `Library` and `Approvals`, including success/degraded paths and approvals parity between the sidebar and the Remote Admin Console.
|
||||
- Completed full verification gate pass (detect-secrets, pre-commit, backend unit suites, adversarial/retry/real-backend lanes, and frontend Playwright E2E).
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
|
||||
<summary><strong>Audit event clarity and connector ingress fail-closed hardening</strong></summary>
|
||||
|
||||
- Normalized audit helper behavior so config/secret/LLM-test convenience wrappers now emit one canonical audit event per action, reducing duplicate noise while preserving legacy compatibility paths.
|
||||
|
||||
Generated
+2078
-1
File diff suppressed because it is too large
Load Diff
+5
-1
@@ -5,10 +5,14 @@
|
||||
"node": ">=18.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@playwright/test": "^1.50.0"
|
||||
"@playwright/test": "^1.50.0",
|
||||
"jsdom": "^26.0.0",
|
||||
"vitest": "^3.2.4"
|
||||
},
|
||||
"scripts": {
|
||||
"test": "node scripts/run-playwright.mjs",
|
||||
"test:unit": "vitest run",
|
||||
"test:unit:watch": "vitest",
|
||||
"test:ui": "npx playwright test --ui",
|
||||
"test:headed": "npx playwright test --headed",
|
||||
"test:debug": "npx playwright test --debug",
|
||||
|
||||
@@ -0,0 +1,124 @@
|
||||
import { expect, test } from '@playwright/test';
|
||||
import { clickTab, mockComfyUiCore, waitForOpenClawReady } from '../utils/helpers.js';
|
||||
|
||||
const pendingApproval = {
|
||||
approval_id: 'apr-001',
|
||||
template_id: 'render_portrait',
|
||||
status: 'pending',
|
||||
requested_at: '2026-03-05T10:00:00Z',
|
||||
source: 'telegram',
|
||||
inputs: { prompt: 'portrait', style: 'studio' },
|
||||
};
|
||||
|
||||
async function mockApprovalApis(page, { listStatus = 200, listData = [pendingApproval], approveStatus = 200 } = {}) {
|
||||
let approvals = [...listData];
|
||||
|
||||
await page.route('**/openclaw/approvals**', async (route) => {
|
||||
const request = route.request();
|
||||
const url = new URL(request.url());
|
||||
|
||||
if (request.method() === 'GET' && /\/approvals\/[^/]+$/.test(url.pathname)) {
|
||||
const id = decodeURIComponent(url.pathname.split('/').pop());
|
||||
const match = approvals.find((item) => item.approval_id === id);
|
||||
await route.fulfill({
|
||||
status: match ? 200 : 404,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify(match ? { approval: match } : { error: 'not_found' }),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (request.method() === 'GET') {
|
||||
await route.fulfill({
|
||||
status: listStatus,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify(
|
||||
listStatus === 200 ? { approvals } : { error: 'approval_list_failed' }
|
||||
),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (request.method() === 'POST' && url.pathname.endsWith('/approve')) {
|
||||
if (approveStatus === 200) {
|
||||
approvals = approvals.map((item) =>
|
||||
item.approval_id === pendingApproval.approval_id
|
||||
? { ...item, status: 'approved' }
|
||||
: item
|
||||
);
|
||||
}
|
||||
await route.fulfill({
|
||||
status: approveStatus,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify(
|
||||
approveStatus === 200
|
||||
? { executed: true, prompt_id: 'prompt-42' }
|
||||
: { error: 'approve_failed' }
|
||||
),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (request.method() === 'POST' && url.pathname.endsWith('/reject')) {
|
||||
approvals = approvals.map((item) =>
|
||||
item.approval_id === pendingApproval.approval_id
|
||||
? { ...item, status: 'rejected' }
|
||||
: item
|
||||
);
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify({ ok: true }),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
await route.fulfill({ status: 404, contentType: 'application/json', body: JSON.stringify({ error: 'not_found' }) });
|
||||
});
|
||||
}
|
||||
|
||||
test.describe('Approvals surfaces', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await mockComfyUiCore(page);
|
||||
await page.addInitScript(() => {
|
||||
window.confirm = () => true;
|
||||
window.alert = () => {};
|
||||
});
|
||||
});
|
||||
|
||||
test('loads pending approvals and allows approve action', async ({ page }) => {
|
||||
await mockApprovalApis(page);
|
||||
await page.goto('test-harness.html');
|
||||
await waitForOpenClawReady(page);
|
||||
await clickTab(page, 'Approvals');
|
||||
|
||||
await expect(page.locator('#apr-list .openclaw-list-item')).toHaveCount(1);
|
||||
await expect(page.locator('#apr-list')).toContainText('render_portrait');
|
||||
|
||||
await page.locator('#apr-list button[data-action="approve"]').click();
|
||||
await expect(page.locator('#apr-list')).toContainText('APPROVED');
|
||||
});
|
||||
|
||||
test('shows approval list fetch failures inside the sidebar', async ({ page }) => {
|
||||
await mockApprovalApis(page, { listStatus: 500, listData: [] });
|
||||
await page.goto('test-harness.html');
|
||||
await waitForOpenClawReady(page);
|
||||
await clickTab(page, 'Approvals');
|
||||
|
||||
await expect(page.locator('.openclaw-error-box')).toContainText('approval_list_failed');
|
||||
});
|
||||
|
||||
test('keeps admin console pending approvals aligned with sidebar data', async ({ page }) => {
|
||||
await mockApprovalApis(page);
|
||||
await page.goto('test-harness.html');
|
||||
await waitForOpenClawReady(page);
|
||||
await clickTab(page, 'Approvals');
|
||||
await expect(page.locator('#apr-list')).toContainText('apr-001');
|
||||
|
||||
await page.goto('http://127.0.0.1:3000/web/admin_console.html');
|
||||
await page.locator('#refreshApprovals').click();
|
||||
|
||||
await expect(page.locator('#approvalsList')).toContainText('apr-001');
|
||||
await expect(page.locator('#approvalsList')).toContainText('render_portrait');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,120 @@
|
||||
import { expect, test } from '@playwright/test';
|
||||
import { clickTab, mockComfyUiCore, waitForOpenClawReady } from '../utils/helpers.js';
|
||||
|
||||
const presets = [
|
||||
{
|
||||
id: 'prompt-1',
|
||||
name: 'Portrait Prompt',
|
||||
category: 'prompt',
|
||||
content: { positive: 'portrait lighting', negative: 'blurry' },
|
||||
},
|
||||
{
|
||||
id: 'params-1',
|
||||
name: 'Landscape Params',
|
||||
category: 'params',
|
||||
content: { params: { width: 1280, height: 720, seed: 99 } },
|
||||
},
|
||||
];
|
||||
|
||||
test.describe('Library Tab', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await mockComfyUiCore(page);
|
||||
await page.addInitScript(() => {
|
||||
window.confirm = () => true;
|
||||
window.alert = () => {};
|
||||
});
|
||||
|
||||
await page.route('**/openclaw/presets**', async (route) => {
|
||||
const request = route.request();
|
||||
const url = new URL(request.url());
|
||||
|
||||
if (request.method() === 'GET' && /\/presets\/[^/]+$/.test(url.pathname)) {
|
||||
const id = decodeURIComponent(url.pathname.split('/').pop());
|
||||
const preset = presets.find((item) => item.id === id);
|
||||
await route.fulfill({
|
||||
status: preset ? 200 : 404,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify(preset || { error: 'not_found' }),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (request.method() === 'GET') {
|
||||
const category = url.searchParams.get('category');
|
||||
const items = category ? presets.filter((item) => item.category === category) : presets;
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify(items),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (request.method() === 'POST') {
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify({ ok: true, id: 'new-preset' }),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
await route.fulfill({ status: 204, body: '' });
|
||||
});
|
||||
|
||||
await page.route('**/openclaw/packs**', async (route) => {
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify({ packs: [] }),
|
||||
});
|
||||
});
|
||||
|
||||
await page.goto('test-harness.html');
|
||||
await waitForOpenClawReady(page);
|
||||
});
|
||||
|
||||
test('loads presets and applies prompt presets into Planner', async ({ page }) => {
|
||||
await clickTab(page, 'Library');
|
||||
|
||||
await expect(page.locator('#lib-list .openclaw-list-item')).toHaveCount(2);
|
||||
await expect(page.locator('#lib-list')).toContainText('Portrait Prompt');
|
||||
|
||||
await page.locator('#lib-list button[data-action="apply"]').first().click();
|
||||
|
||||
await expect(page.locator('#openclaw-tab-planner')).toHaveClass(/active/);
|
||||
await expect(page.locator('#planner-out-pos')).toHaveValue('portrait lighting');
|
||||
await expect(page.locator('#planner-out-neg')).toHaveValue('blurry');
|
||||
});
|
||||
|
||||
test('filters presets and routes params presets into Variants', async ({ page }) => {
|
||||
await clickTab(page, 'Library');
|
||||
|
||||
await page.locator('#lib-search').fill('landscape');
|
||||
await expect(page.locator('#lib-list .openclaw-list-item')).toHaveCount(1);
|
||||
await expect(page.locator('#lib-list')).toContainText('Landscape Params');
|
||||
|
||||
await page.locator('#lib-list button[data-action="apply"]').click();
|
||||
|
||||
await expect(page.locator('#openclaw-tab-variants')).toHaveClass(/active/);
|
||||
await expect(page.locator('#var-base-params')).toHaveValue(/1280/);
|
||||
await expect(page.locator('#var-base-params')).toHaveValue(/99/);
|
||||
});
|
||||
|
||||
test('shows a deterministic error state when presets fail to load', async ({ page }) => {
|
||||
await page.unroute('**/openclaw/presets**');
|
||||
await page.route('**/openclaw/presets**', async (route) => {
|
||||
await route.fulfill({
|
||||
status: 500,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify({ error: 'preset_list_failed' }),
|
||||
});
|
||||
});
|
||||
|
||||
await page.goto('test-harness.html');
|
||||
await waitForOpenClawReady(page);
|
||||
await clickTab(page, 'Library');
|
||||
|
||||
await expect(page.locator('.openclaw-error-box')).toContainText('preset_list_failed');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,11 @@
|
||||
import { defineConfig } from "vitest/config";
|
||||
|
||||
export default defineConfig({
|
||||
test: {
|
||||
environment: "jsdom",
|
||||
setupFiles: ["./web/tests/unit/setup.js"],
|
||||
include: ["web/tests/unit/**/*.test.js"],
|
||||
restoreMocks: true,
|
||||
clearMocks: true,
|
||||
},
|
||||
});
|
||||
+3
-1854
File diff suppressed because it is too large
Load Diff
+12
-4
@@ -3,6 +3,7 @@
|
||||
* Handles tab creation, switching, and lazy rendering.
|
||||
*/
|
||||
import { ErrorBoundary } from "./ErrorBoundary.js";
|
||||
import { normalizeLegacyClassNames } from "./openclaw_utils.js";
|
||||
|
||||
export class TabManager {
|
||||
constructor() {
|
||||
@@ -49,12 +50,12 @@ export class TabManager {
|
||||
|
||||
this.tabs.forEach(tab => {
|
||||
const btn = document.createElement("div");
|
||||
btn.className = "openclaw-tab moltbot-tab";
|
||||
btn.className = "openclaw-tab";
|
||||
if (tab.icon) {
|
||||
const icon = document.createElement("i");
|
||||
icon.className = `openclaw-tab-icon moltbot-tab-icon ${tab.icon}`;
|
||||
icon.className = `openclaw-tab-icon ${tab.icon}`;
|
||||
const label = document.createElement("span");
|
||||
label.className = "openclaw-tab-label moltbot-tab-label";
|
||||
label.className = "openclaw-tab-label";
|
||||
label.textContent = tab.title;
|
||||
btn.appendChild(icon);
|
||||
btn.appendChild(label);
|
||||
@@ -70,10 +71,13 @@ export class TabManager {
|
||||
if (!this.contentEl.querySelector(`#openclaw-tab-${tab.id}`)) {
|
||||
const pane = document.createElement("div");
|
||||
pane.id = `openclaw-tab-${tab.id}`;
|
||||
pane.className = "openclaw-tab-pane moltbot-tab-pane";
|
||||
pane.className = "openclaw-tab-pane";
|
||||
this.contentEl.appendChild(pane);
|
||||
}
|
||||
});
|
||||
|
||||
normalizeLegacyClassNames(this.tabsEl);
|
||||
normalizeLegacyClassNames(this.contentEl);
|
||||
}
|
||||
|
||||
activateTab(id) {
|
||||
@@ -111,6 +115,10 @@ export class TabManager {
|
||||
});
|
||||
tab.loaded = true;
|
||||
}
|
||||
|
||||
if (pane) {
|
||||
normalizeLegacyClassNames(pane);
|
||||
}
|
||||
}
|
||||
|
||||
_restoreActiveTab() {
|
||||
|
||||
+26
-23
@@ -5,6 +5,7 @@
|
||||
import { tabManager } from "./openclaw_tabs.js";
|
||||
import { ErrorBoundary } from "./ErrorBoundary.js";
|
||||
import { openclawApi } from "./openclaw_api.js";
|
||||
import { normalizeLegacyClassNames } from "./openclaw_utils.js";
|
||||
|
||||
export class OpenClawUI {
|
||||
constructor() {
|
||||
@@ -27,7 +28,7 @@ export class OpenClawUI {
|
||||
}
|
||||
|
||||
_enforceSidebarMinWidth(container) {
|
||||
// IMPORTANT: Keep this value aligned with CSS .openclaw-sidebar-container/.moltbot-sidebar-container min-width.
|
||||
// IMPORTANT: Keep this value aligned with CSS .openclaw-sidebar-container min-width.
|
||||
const minWidthPx = 560;
|
||||
|
||||
const applyMinWidth = () => {
|
||||
@@ -62,10 +63,10 @@ export class OpenClawUI {
|
||||
toggleFloatingPanel() {
|
||||
if (!this.floating.panel) {
|
||||
const panel = document.createElement("div");
|
||||
panel.className = "openclaw-floating-panel moltbot-floating-panel";
|
||||
panel.className = "openclaw-floating-panel";
|
||||
|
||||
const close = document.createElement("button");
|
||||
close.className = "openclaw-floating-close moltbot-floating-close";
|
||||
close.className = "openclaw-floating-close";
|
||||
close.textContent = "\u00D7";
|
||||
close.title = "Close";
|
||||
close.addEventListener("click", () => {
|
||||
@@ -73,7 +74,7 @@ export class OpenClawUI {
|
||||
});
|
||||
|
||||
const content = document.createElement("div");
|
||||
content.className = "openclaw-floating-content moltbot-floating-content";
|
||||
content.className = "openclaw-floating-content";
|
||||
|
||||
panel.appendChild(close);
|
||||
panel.appendChild(content);
|
||||
@@ -98,31 +99,31 @@ export class OpenClawUI {
|
||||
|
||||
_render(container) {
|
||||
container.innerHTML = "";
|
||||
container.className = "openclaw-sidebar-container moltbot-sidebar-container";
|
||||
container.className = "openclaw-sidebar-container";
|
||||
|
||||
// 1. Header
|
||||
const header = document.createElement("div");
|
||||
header.className = "openclaw-header moltbot-header";
|
||||
header.className = "openclaw-header";
|
||||
|
||||
const statusDot = document.createElement("div");
|
||||
statusDot.className = "openclaw-status-dot moltbot-status-dot ok";
|
||||
statusDot.className = "openclaw-status-dot ok";
|
||||
statusDot.title = "System Status";
|
||||
this.statusDot = statusDot;
|
||||
|
||||
const title = document.createElement("div");
|
||||
title.className = "openclaw-title moltbot-title";
|
||||
title.className = "openclaw-title";
|
||||
title.textContent = "OpenClaw";
|
||||
|
||||
// F9: About badges (version fetched from /openclaw/health; legacy /moltbot/health)
|
||||
const badges = document.createElement("div");
|
||||
badges.className = "openclaw-badges moltbot-badges";
|
||||
badges.className = "openclaw-badges";
|
||||
const versionSpan = document.createElement("span");
|
||||
versionSpan.className = "openclaw-version moltbot-version";
|
||||
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 moltbot-repo-link";
|
||||
repoLink.className = "openclaw-repo-link";
|
||||
repoLink.title = "View on GitHub";
|
||||
repoLink.textContent = "View on GitHub";
|
||||
badges.appendChild(versionSpan);
|
||||
@@ -139,7 +140,7 @@ export class OpenClawUI {
|
||||
// F55: Control plane mode indicator badge
|
||||
const cpMode = data?.control_plane?.mode || data?.deployment_profile || "local";
|
||||
const modeBadge = document.createElement("span");
|
||||
modeBadge.className = `openclaw-mode-badge moltbot-mode-badge openclaw-mode-${cpMode} moltbot-mode-${cpMode}`;
|
||||
modeBadge.className = `openclaw-mode-badge openclaw-mode-${cpMode}`;
|
||||
modeBadge.textContent = cpMode.toUpperCase();
|
||||
modeBadge.title = `Control plane: ${cpMode}`;
|
||||
// Style inline for immediate visibility
|
||||
@@ -183,18 +184,19 @@ export class OpenClawUI {
|
||||
|
||||
// 2. Tab Bar
|
||||
const tabBar = document.createElement("div");
|
||||
tabBar.className = "openclaw-tabs moltbot-tabs";
|
||||
tabBar.className = "openclaw-tabs";
|
||||
this.tabBar = tabBar;
|
||||
container.appendChild(tabBar);
|
||||
|
||||
// 3. Content Area
|
||||
const contentArea = document.createElement("div");
|
||||
contentArea.className = "openclaw-content moltbot-content";
|
||||
contentArea.className = "openclaw-content";
|
||||
this.contentArea = contentArea;
|
||||
container.appendChild(contentArea);
|
||||
|
||||
// Initialize Tabs
|
||||
tabManager.init(tabBar, contentArea);
|
||||
normalizeLegacyClassNames(container);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -268,7 +270,7 @@ export class OpenClawUI {
|
||||
header.after(bannerEl);
|
||||
}
|
||||
|
||||
bannerEl.className = `openclaw-banner moltbot-banner openclaw-banner-${severity} moltbot-banner-${severity}`;
|
||||
bannerEl.className = `openclaw-banner openclaw-banner-${severity}`;
|
||||
bannerEl.dataset.id = id;
|
||||
bannerEl.dataset.severity = severity;
|
||||
bannerEl.innerHTML = ""; // Clear content
|
||||
@@ -281,7 +283,7 @@ export class OpenClawUI {
|
||||
// Action Button
|
||||
if (action) {
|
||||
const btn = document.createElement("button");
|
||||
btn.className = "openclaw-banner-action moltbot-banner-action";
|
||||
btn.className = "openclaw-banner-action";
|
||||
btn.textContent = action.label;
|
||||
btn.addEventListener("click", () => this.handleAction(action));
|
||||
bannerEl.appendChild(btn);
|
||||
@@ -290,7 +292,7 @@ export class OpenClawUI {
|
||||
// Dismiss Button
|
||||
if (dismissible) {
|
||||
const close = document.createElement("button");
|
||||
close.className = "openclaw-banner-close moltbot-banner-close";
|
||||
close.className = "openclaw-banner-close";
|
||||
close.textContent = "\u00D7";
|
||||
close.addEventListener("click", () => {
|
||||
bannerEl.remove();
|
||||
@@ -345,10 +347,10 @@ export class OpenClawUI {
|
||||
showConfirm({ title, message, fatal = false, onConfirm }) {
|
||||
// Create modal overlay
|
||||
const overlay = document.createElement("div");
|
||||
overlay.className = "openclaw-modal-overlay moltbot-modal-overlay";
|
||||
overlay.className = "openclaw-modal-overlay";
|
||||
|
||||
const modal = document.createElement("div");
|
||||
modal.className = `openclaw-modal moltbot-modal ${fatal ? "fatal" : ""}`;
|
||||
modal.className = `openclaw-modal ${fatal ? "fatal" : ""}`;
|
||||
|
||||
const h3 = document.createElement("h3");
|
||||
h3.textContent = title || "Confirm Action";
|
||||
@@ -357,15 +359,15 @@ export class OpenClawUI {
|
||||
p.textContent = message || "Are you sure?";
|
||||
|
||||
const buttons = document.createElement("div");
|
||||
buttons.className = "openclaw-modal-buttons moltbot-modal-buttons";
|
||||
buttons.className = "openclaw-modal-buttons";
|
||||
|
||||
const cancelBtn = document.createElement("button");
|
||||
cancelBtn.className = "openclaw-btn moltbot-btn secondary";
|
||||
cancelBtn.className = "openclaw-btn secondary";
|
||||
cancelBtn.textContent = "Cancel";
|
||||
cancelBtn.onclick = () => overlay.remove();
|
||||
|
||||
const confirmBtn = document.createElement("button");
|
||||
confirmBtn.className = `openclaw-btn moltbot-btn ${fatal ? "danger" : "primary"}`;
|
||||
confirmBtn.className = `openclaw-btn ${fatal ? "danger" : "primary"}`;
|
||||
confirmBtn.textContent = "Confirm";
|
||||
confirmBtn.onclick = () => {
|
||||
overlay.remove();
|
||||
@@ -381,6 +383,7 @@ export class OpenClawUI {
|
||||
overlay.appendChild(modal);
|
||||
|
||||
this.container.appendChild(overlay);
|
||||
normalizeLegacyClassNames(overlay);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -574,7 +577,7 @@ export class OpenClawActions {
|
||||
*/
|
||||
_showBlockedToast(actionName, reason) {
|
||||
const toast = document.createElement("div");
|
||||
toast.className = "openclaw-blocked-toast moltbot-blocked-toast";
|
||||
toast.className = "openclaw-blocked-toast";
|
||||
toast.style.cssText = `
|
||||
position: fixed; bottom: 20px; right: 20px; z-index: 99999;
|
||||
background: #1e1e2e; border: 1px solid #f59e0b;
|
||||
|
||||
@@ -18,6 +18,59 @@ export function makeEl(tag, className = "", text = "") {
|
||||
return el;
|
||||
}
|
||||
|
||||
/**
|
||||
* F63: prefer canonical `openclaw-*` classes when both legacy and canonical
|
||||
* variants are present on the same node.
|
||||
*/
|
||||
export function normalizeLegacyClassTokens(className = "") {
|
||||
const tokens = String(className)
|
||||
.split(/\s+/)
|
||||
.map((token) => token.trim())
|
||||
.filter(Boolean);
|
||||
const canonical = new Set(
|
||||
tokens.filter((token) => token.startsWith("openclaw-"))
|
||||
);
|
||||
const seen = new Set();
|
||||
const normalized = [];
|
||||
|
||||
tokens.forEach((token) => {
|
||||
if (token.startsWith("moltbot-")) {
|
||||
const suffix = token.slice("moltbot-".length);
|
||||
if (canonical.has(`openclaw-${suffix}`)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (seen.has(token)) {
|
||||
return;
|
||||
}
|
||||
seen.add(token);
|
||||
normalized.push(token);
|
||||
});
|
||||
|
||||
return normalized.join(" ");
|
||||
}
|
||||
|
||||
export function normalizeLegacyClassNames(root) {
|
||||
if (!root) return root;
|
||||
const nodes = [];
|
||||
if (typeof root.className === "string") {
|
||||
nodes.push(root);
|
||||
}
|
||||
if (typeof root.querySelectorAll === "function") {
|
||||
nodes.push(...root.querySelectorAll("[class]"));
|
||||
}
|
||||
|
||||
nodes.forEach((node) => {
|
||||
if (typeof node.className !== "string") return;
|
||||
const normalized = normalizeLegacyClassTokens(node.className);
|
||||
if (normalized !== node.className) {
|
||||
node.className = normalized;
|
||||
}
|
||||
});
|
||||
|
||||
return root;
|
||||
}
|
||||
|
||||
/**
|
||||
* Lightweight toast helper for UI feedback.
|
||||
* @param {string} message
|
||||
|
||||
@@ -0,0 +1,926 @@
|
||||
/* Moltbot Design System */
|
||||
|
||||
:root {
|
||||
/* Spacing Scale */
|
||||
--openclaw-space-xs: 4px;
|
||||
--openclaw-space-sm: 8px;
|
||||
--openclaw-space-md: 12px;
|
||||
--openclaw-space-lg: 16px;
|
||||
--openclaw-space-xl: 24px;
|
||||
|
||||
/* Typography */
|
||||
--openclaw-font-xs: 11px;
|
||||
--openclaw-font-sm: 12px;
|
||||
--openclaw-font-md: 13px;
|
||||
--openclaw-font-lg: 14px;
|
||||
--openclaw-font-mono: 'Consolas', 'Monaco', 'Courier New', monospace;
|
||||
|
||||
/* Colors (inheriting from ComfyUI variable names where possible, fallbacks provided) */
|
||||
--openclaw-color-bg: var(--comfy-menu-bg, #222);
|
||||
--openclaw-color-bg-light: var(--comfy-input-bg, #333);
|
||||
--openclaw-color-fg: var(--fg-color, #eee);
|
||||
--openclaw-color-fg-muted: #aaa;
|
||||
--openclaw-color-border: var(--border-color, #444);
|
||||
--openclaw-color-primary: var(--primary-color, #2a2);
|
||||
--openclaw-color-danger: #d44;
|
||||
--openclaw-color-warning: #ea0;
|
||||
--openclaw-color-success: #2a2;
|
||||
}
|
||||
|
||||
/* Layout Primitives */
|
||||
|
||||
.openclaw-sidebar-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
min-width: 560px; /* Keep sidebar readable: header tabs + Parameter Lab controls */
|
||||
min-height: 0; /* allow children to shrink for overflow handling */
|
||||
background: var(--openclaw-color-bg);
|
||||
color: var(--openclaw-color-fg);
|
||||
font-family: Arial, sans-serif;
|
||||
font-size: var(--openclaw-font-md);
|
||||
}
|
||||
|
||||
/* CRITICAL: Keep this rule + !important; SplitterPanel inline sizing can override inner container min-width. */
|
||||
.side-bar-panel:has(.openclaw-sidebar-container) {
|
||||
min-width: 560px !important;
|
||||
}
|
||||
|
||||
.openclaw-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: var(--openclaw-space-md);
|
||||
padding: var(--openclaw-space-md) var(--openclaw-space-lg);
|
||||
border-bottom: 1px solid var(--openclaw-color-border);
|
||||
background: rgba(0,0,0,0.15);
|
||||
}
|
||||
|
||||
.openclaw-status-dot {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border-radius: 50%;
|
||||
background: #666;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
.openclaw-status-dot.ok { background: #2a2; }
|
||||
.openclaw-status-dot.warn { background: #ea0; }
|
||||
.openclaw-status-dot.err { background: #c44; }
|
||||
|
||||
.openclaw-title {
|
||||
font-size: var(--openclaw-font-lg);
|
||||
font-weight: 700;
|
||||
flex: 1 1 auto;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.openclaw-badges {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
gap: var(--openclaw-space-sm);
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.openclaw-version {
|
||||
font-size: var(--openclaw-font-sm);
|
||||
color: var(--openclaw-color-fg-muted);
|
||||
}
|
||||
|
||||
.openclaw-repo-link {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 4px 8px;
|
||||
border: 1px solid var(--openclaw-color-border);
|
||||
border-radius: 999px;
|
||||
background: rgba(255,255,255,0.04);
|
||||
color: var(--openclaw-color-fg);
|
||||
text-decoration: none;
|
||||
font-size: var(--openclaw-font-sm);
|
||||
white-space: nowrap;
|
||||
}
|
||||
.openclaw-repo-link:hover { background: rgba(255,255,255,0.08); }
|
||||
|
||||
.openclaw-banner {
|
||||
padding: var(--openclaw-space-sm) var(--openclaw-space-lg);
|
||||
border-bottom: 1px solid var(--openclaw-color-border);
|
||||
font-size: var(--openclaw-font-sm);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--openclaw-space-md);
|
||||
justify-content: space-between;
|
||||
}
|
||||
.openclaw-banner > span { flex: 1; }
|
||||
|
||||
.openclaw-banner-warning { background: rgba(255, 170, 0, 0.10); color: #ffd27a; }
|
||||
.openclaw-banner-info { background: rgba(0, 120, 255, 0.10); color: #a8d1ff; }
|
||||
.openclaw-banner-success { background: rgba(42, 170, 42, 0.15); color: #bfffc0; }
|
||||
.openclaw-banner-error { background: rgba(200, 50, 50, 0.15); color: #ffb4b4; }
|
||||
|
||||
.openclaw-banner-action {
|
||||
background: rgba(255,255,255,0.1);
|
||||
border: 1px solid rgba(255,255,255,0.2);
|
||||
color: inherit;
|
||||
padding: 2px 8px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: var(--openclaw-font-xs);
|
||||
text-transform: uppercase;
|
||||
font-weight: bold;
|
||||
}
|
||||
.openclaw-banner-action:hover { background: rgba(255,255,255,0.2); }
|
||||
|
||||
.openclaw-banner-close {
|
||||
background: none;
|
||||
border: none;
|
||||
color: inherit;
|
||||
opacity: 0.7;
|
||||
cursor: pointer;
|
||||
font-size: 16px;
|
||||
line-height: 1;
|
||||
padding: 0 4px;
|
||||
}
|
||||
.openclaw-banner-close:hover { opacity: 1; }
|
||||
|
||||
.openclaw-tabs {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr)); /* 4 tabs per row */
|
||||
grid-auto-flow: row; /* left-to-right, then top-to-bottom */
|
||||
gap: 6px;
|
||||
padding: 8px var(--openclaw-space-lg);
|
||||
border-bottom: 1px solid var(--openclaw-color-border);
|
||||
background: rgba(0,0,0,0.10);
|
||||
}
|
||||
|
||||
.openclaw-tab {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 6px 10px;
|
||||
border-radius: 6px;
|
||||
border: 1px solid transparent;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
color: var(--openclaw-color-fg-muted);
|
||||
background: rgba(255,255,255,0.02);
|
||||
font-size: var(--openclaw-font-sm);
|
||||
line-height: 1;
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.openclaw-tab:hover {
|
||||
color: var(--openclaw-color-fg);
|
||||
border-color: rgba(255,255,255,0.08);
|
||||
background: rgba(255,255,255,0.04);
|
||||
}
|
||||
|
||||
.openclaw-tab.active {
|
||||
color: var(--openclaw-color-fg);
|
||||
border-color: rgba(42, 170, 42, 0.5);
|
||||
background: rgba(42, 170, 42, 0.15);
|
||||
}
|
||||
|
||||
.openclaw-tab-icon {
|
||||
font-size: 13px;
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.openclaw-tab-label {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.openclaw-content {
|
||||
flex: 1 1 auto;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.openclaw-tab-pane {
|
||||
display: none;
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
}
|
||||
.openclaw-tab-pane.active {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.openclaw-panel {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
color: var(--openclaw-color-fg);
|
||||
font-size: var(--openclaw-font-md);
|
||||
background: var(--openclaw-color-bg);
|
||||
}
|
||||
|
||||
.openclaw-scroll-area {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: var(--openclaw-space-lg); /* More breathing room */
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--openclaw-space-lg);
|
||||
}
|
||||
|
||||
.openclaw-card {
|
||||
background: rgba(0, 0, 0, 0.2);
|
||||
border: 1px solid var(--openclaw-color-border);
|
||||
border-radius: 4px;
|
||||
padding: var(--openclaw-space-md);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--openclaw-space-sm);
|
||||
}
|
||||
|
||||
.openclaw-section-header {
|
||||
font-size: var(--openclaw-font-sm);
|
||||
text-transform: uppercase;
|
||||
color: var(--openclaw-color-fg-muted);
|
||||
letter-spacing: 0.05em;
|
||||
margin-bottom: var(--openclaw-space-xs);
|
||||
font-weight: bold;
|
||||
border-bottom: 1px solid var(--openclaw-color-border);
|
||||
padding-bottom: var(--openclaw-space-xs);
|
||||
}
|
||||
|
||||
/* Grids & Splits */
|
||||
.openclaw-grid-2 {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: var(--openclaw-space-md);
|
||||
}
|
||||
|
||||
.openclaw-split-v {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--openclaw-space-md);
|
||||
}
|
||||
|
||||
.openclaw-split-h {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: var(--openclaw-space-md);
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
/* Form Elements */
|
||||
.openclaw-input-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--openclaw-space-xs);
|
||||
}
|
||||
|
||||
.openclaw-section {
|
||||
background: rgba(0,0,0,0.18);
|
||||
border: 1px solid var(--openclaw-color-border);
|
||||
border-radius: 6px;
|
||||
padding: var(--openclaw-space-lg);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--openclaw-space-md);
|
||||
}
|
||||
|
||||
.openclaw-section > h4 {
|
||||
margin: 0;
|
||||
font-size: var(--openclaw-font-lg);
|
||||
}
|
||||
|
||||
.openclaw-form-row {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--openclaw-space-xs);
|
||||
}
|
||||
|
||||
.openclaw-btn-row {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: var(--openclaw-space-sm);
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.openclaw-note {
|
||||
padding: var(--openclaw-space-sm) var(--openclaw-space-md);
|
||||
border: 1px solid var(--openclaw-color-border);
|
||||
border-radius: 4px;
|
||||
background: rgba(255,255,255,0.03);
|
||||
color: var(--openclaw-color-fg-muted);
|
||||
font-size: var(--openclaw-font-sm);
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
.openclaw-status {
|
||||
font-size: var(--openclaw-font-sm);
|
||||
color: var(--openclaw-color-fg-muted);
|
||||
min-height: 16px;
|
||||
}
|
||||
.openclaw-status.ok { color: #bfffc0; }
|
||||
.openclaw-status.error { color: #ffb4b4; }
|
||||
|
||||
.openclaw-kv-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: var(--openclaw-space-md);
|
||||
}
|
||||
.openclaw-kv-key { color: var(--openclaw-color-fg-muted); }
|
||||
.openclaw-kv-val { color: var(--openclaw-color-fg); }
|
||||
.openclaw-kv-val.ok { color: #bfffc0; }
|
||||
.openclaw-kv-val.error { color: #ffb4b4; }
|
||||
|
||||
.openclaw-log-viewer {
|
||||
white-space: pre-wrap;
|
||||
font-family: var(--openclaw-font-mono);
|
||||
font-size: var(--openclaw-font-sm);
|
||||
background: rgba(0,0,0,0.25);
|
||||
border: 1px solid var(--openclaw-color-border);
|
||||
border-radius: 4px;
|
||||
padding: var(--openclaw-space-md);
|
||||
max-height: 260px;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.openclaw-checkbox {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.openclaw-help-btn {
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
border-radius: 999px;
|
||||
border: 1px solid var(--openclaw-color-border);
|
||||
background: rgba(255,255,255,0.04);
|
||||
color: var(--openclaw-color-fg);
|
||||
cursor: pointer;
|
||||
font-size: var(--openclaw-font-sm);
|
||||
line-height: 1;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
.openclaw-help-btn:hover {
|
||||
background: rgba(255,255,255,0.08);
|
||||
}
|
||||
|
||||
.openclaw-label {
|
||||
font-size: var(--openclaw-font-sm);
|
||||
color: var(--openclaw-color-fg-muted);
|
||||
}
|
||||
|
||||
.openclaw-input, .openclaw-select, .openclaw-textarea {
|
||||
background: var(--openclaw-color-bg-light);
|
||||
border: 1px solid var(--openclaw-color-border);
|
||||
color: var(--openclaw-color-fg);
|
||||
padding: 6px 8px;
|
||||
border-radius: 2px;
|
||||
font-size: var(--openclaw-font-md);
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.openclaw-input:focus, .openclaw-select:focus, .openclaw-textarea:focus {
|
||||
border-color: var(--openclaw-color-primary);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.openclaw-textarea {
|
||||
font-family: var(--openclaw-font-mono);
|
||||
resize: vertical;
|
||||
min-height: 60px;
|
||||
}
|
||||
|
||||
.openclaw-textarea-sm { min-height: 80px; }
|
||||
.openclaw-textarea-md { min-height: 150px; }
|
||||
.openclaw-textarea-lg { min-height: 250px; }
|
||||
.openclaw-textarea-xl { min-height: 400px; }
|
||||
|
||||
/* Buttons */
|
||||
.openclaw-btn {
|
||||
background: var(--openclaw-color-bg-light);
|
||||
border: 1px solid var(--openclaw-color-border);
|
||||
color: var(--openclaw-color-fg);
|
||||
padding: 6px 12px;
|
||||
cursor: pointer;
|
||||
font-size: var(--openclaw-font-md);
|
||||
border-radius: 2px;
|
||||
text-align: center;
|
||||
transition: background 0.1s;
|
||||
}
|
||||
|
||||
.openclaw-btn:hover:not(:disabled) {
|
||||
background: #444;
|
||||
}
|
||||
|
||||
.openclaw-btn:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.openclaw-btn-primary {
|
||||
background: #2a2; /* Green-ish */
|
||||
color: #fff;
|
||||
border-color: #181;
|
||||
}
|
||||
.openclaw-btn-primary:hover:not(:disabled) { background: #3b3; }
|
||||
|
||||
.openclaw-btn-danger {
|
||||
background: #a33;
|
||||
color: #fff;
|
||||
border-color: #822;
|
||||
}
|
||||
.openclaw-btn-danger:hover:not(:disabled) { background: #c44; }
|
||||
|
||||
.openclaw-btn-sm {
|
||||
padding: 2px 6px;
|
||||
font-size: var(--openclaw-font-sm);
|
||||
}
|
||||
|
||||
.openclaw-btn-success {
|
||||
background: rgba(42, 170, 42, 0.2);
|
||||
border-color: rgba(42, 170, 42, 0.6);
|
||||
color: #dfffe0;
|
||||
}
|
||||
|
||||
/* Compatibility aliases used by existing tabs */
|
||||
.openclaw-btn.primary {
|
||||
background: rgba(42, 170, 42, 0.2);
|
||||
border-color: rgba(42, 170, 42, 0.6);
|
||||
color: #dfffe0;
|
||||
}
|
||||
.openclaw-btn.primary:hover:not(:disabled) {
|
||||
background: rgba(42, 170, 42, 0.3);
|
||||
}
|
||||
.openclaw-btn.secondary {
|
||||
background: rgba(255, 255, 255, 0.04);
|
||||
border-color: var(--openclaw-color-border);
|
||||
color: var(--openclaw-color-fg);
|
||||
}
|
||||
|
||||
.openclaw-btn.has-icon {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.openclaw-btn-icon {
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
border: 1px solid var(--openclaw-color-border);
|
||||
background: rgba(255, 255, 255, 0.04);
|
||||
color: var(--openclaw-color-fg);
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: var(--openclaw-font-sm);
|
||||
line-height: 1;
|
||||
}
|
||||
.openclaw-btn-icon:hover {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.openclaw-separator {
|
||||
width: 1px;
|
||||
height: 24px;
|
||||
background: var(--openclaw-color-border);
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.openclaw-form-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.openclaw-form-group > label {
|
||||
font-size: var(--openclaw-font-sm);
|
||||
color: var(--openclaw-color-fg-muted);
|
||||
}
|
||||
|
||||
.openclaw-form-group > input,
|
||||
.openclaw-form-group > select,
|
||||
.openclaw-form-group > textarea {
|
||||
background: var(--openclaw-color-bg-light);
|
||||
border: 1px solid var(--openclaw-color-border);
|
||||
color: var(--openclaw-color-fg);
|
||||
padding: 6px 8px;
|
||||
border-radius: 4px;
|
||||
font-size: var(--openclaw-font-md);
|
||||
}
|
||||
.openclaw-form-group > input:focus,
|
||||
.openclaw-form-group > select:focus,
|
||||
.openclaw-form-group > textarea:focus {
|
||||
outline: none;
|
||||
border-color: rgba(42, 170, 42, 0.7);
|
||||
}
|
||||
|
||||
.openclaw-hint,
|
||||
.openclaw-loading,
|
||||
.openclaw-error {
|
||||
padding: 10px 12px;
|
||||
border-radius: 6px;
|
||||
font-size: var(--openclaw-font-sm);
|
||||
border: 1px solid var(--openclaw-color-border);
|
||||
background: rgba(255, 255, 255, 0.03);
|
||||
color: var(--openclaw-color-fg-muted);
|
||||
}
|
||||
|
||||
.openclaw-loading {
|
||||
color: #d2e4ff;
|
||||
border-color: rgba(90, 150, 255, 0.45);
|
||||
background: rgba(90, 150, 255, 0.12);
|
||||
}
|
||||
|
||||
.openclaw-error {
|
||||
color: #ffb4b4;
|
||||
border-color: rgba(200, 60, 60, 0.6);
|
||||
background: rgba(200, 60, 60, 0.18);
|
||||
}
|
||||
|
||||
/* Status Indicators */
|
||||
.openclaw-badge {
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
font-size: var(--openclaw-font-xs);
|
||||
font-weight: bold;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
.openclaw-badge.pending { background: #aa0; color: #000; }
|
||||
.openclaw-badge.approved { background: #2a2; color: #fff; }
|
||||
.openclaw-badge.rejected { background: #c00; color: #fff; }
|
||||
.openclaw-badge.expired { background: #666; color: #ccc; }
|
||||
|
||||
/* Error / Empty States */
|
||||
.openclaw-empty-state {
|
||||
padding: var(--openclaw-space-xl);
|
||||
text-align: center;
|
||||
color: var(--openclaw-color-fg-muted);
|
||||
border: 1px dashed var(--openclaw-color-border);
|
||||
border-radius: 4px;
|
||||
background: rgba(255, 255, 255, 0.02);
|
||||
}
|
||||
|
||||
.openclaw-error-box {
|
||||
padding: var(--openclaw-space-md);
|
||||
background: rgba(200, 50, 50, 0.1);
|
||||
border: 1px solid #a33;
|
||||
border-radius: 4px;
|
||||
color: #eaa;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--openclaw-space-sm);
|
||||
}
|
||||
|
||||
.openclaw-error-title {
|
||||
font-weight: bold;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--openclaw-space-sm);
|
||||
}
|
||||
|
||||
.openclaw-error-hint {
|
||||
font-size: var(--openclaw-font-sm);
|
||||
color: #ccc;
|
||||
background: rgba(0,0,0,0.3);
|
||||
padding: 4px;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
/* Loading Spinner (Simple) */
|
||||
.openclaw-spinner {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border: 2px solid rgba(255,255,255,0.3);
|
||||
border-radius: 50%;
|
||||
border-top-color: #fff;
|
||||
animation: spin 1s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
/* Modals */
|
||||
.openclaw-modal-overlay {
|
||||
position: fixed;
|
||||
top: 0; left: 0; right: 0; bottom: 0;
|
||||
background: rgba(0,0,0,0.7);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 9999;
|
||||
}
|
||||
|
||||
.openclaw-modal {
|
||||
background: var(--openclaw-color-bg);
|
||||
border: 1px solid var(--openclaw-color-border);
|
||||
box-shadow: 0 10px 25px rgba(0,0,0,0.5);
|
||||
border-radius: 6px;
|
||||
min-width: 400px;
|
||||
max-width: 90vw;
|
||||
max-height: 90vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.openclaw-modal-header {
|
||||
padding: var(--openclaw-space-md);
|
||||
border-bottom: 1px solid var(--openclaw-color-border);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
font-weight: bold;
|
||||
font-size: var(--openclaw-font-lg);
|
||||
}
|
||||
|
||||
.openclaw-modal-body {
|
||||
padding: var(--openclaw-space-lg);
|
||||
overflow-y: auto;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.openclaw-modal-footer {
|
||||
padding: var(--openclaw-space-md);
|
||||
border-top: 1px solid var(--openclaw-color-border);
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: var(--openclaw-space-md);
|
||||
background: rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
/* Parameter Lab */
|
||||
.openclaw-lab-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--openclaw-space-md);
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
padding: var(--openclaw-space-md);
|
||||
box-sizing: border-box;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.openclaw-lab-header {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--openclaw-space-md);
|
||||
border: 1px solid var(--openclaw-color-border);
|
||||
background: rgba(255, 255, 255, 0.03);
|
||||
border-radius: 8px;
|
||||
padding: var(--openclaw-space-md);
|
||||
}
|
||||
|
||||
.openclaw-lab-title-wrap {
|
||||
flex: 1 1 100%;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.openclaw-lab-title-wrap > h3 {
|
||||
margin: 0;
|
||||
font-size: 20px;
|
||||
line-height: 1.1;
|
||||
word-break: normal;
|
||||
overflow-wrap: normal;
|
||||
}
|
||||
|
||||
.openclaw-lab-title-wrap > p {
|
||||
margin: 4px 0 0;
|
||||
color: var(--openclaw-color-fg-muted);
|
||||
font-size: var(--openclaw-font-sm);
|
||||
line-height: 1.35;
|
||||
max-width: 60ch;
|
||||
word-break: normal;
|
||||
overflow-wrap: normal;
|
||||
}
|
||||
|
||||
.openclaw-lab-actions {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
gap: var(--openclaw-space-sm);
|
||||
width: 100%;
|
||||
margin: 0 auto;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.openclaw-lab-action-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 6px;
|
||||
min-width: 0;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.openclaw-lab-actions .openclaw-btn.has-icon.openclaw-lab-action-btn {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.openclaw-lab-action-icon {
|
||||
flex: 0 0 auto;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.openclaw-lab-action-label {
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.openclaw-lab-action-btn.active {
|
||||
border-color: rgba(42, 170, 42, 0.65);
|
||||
background: rgba(42, 170, 42, 0.2);
|
||||
color: #dfffe0;
|
||||
}
|
||||
|
||||
.openclaw-lab-action-btn:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.openclaw-lab-action-btn:focus-visible {
|
||||
border-color: rgba(42, 170, 42, 0.75);
|
||||
box-shadow: 0 0 0 1px rgba(42, 170, 42, 0.35) inset;
|
||||
}
|
||||
|
||||
.openclaw-lab-actions .openclaw-separator {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.openclaw-lab-main {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
gap: var(--openclaw-space-md);
|
||||
min-height: 0;
|
||||
overflow-y: auto;
|
||||
padding-right: 2px;
|
||||
}
|
||||
|
||||
.openclaw-lab-card {
|
||||
border: 1px solid var(--openclaw-color-border);
|
||||
background: rgba(0, 0, 0, 0.16);
|
||||
border-radius: 8px;
|
||||
padding: var(--openclaw-space-md);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--openclaw-space-md);
|
||||
}
|
||||
|
||||
.openclaw-lab-card-head {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
justify-content: space-between;
|
||||
gap: var(--openclaw-space-md);
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.08);
|
||||
padding-bottom: var(--openclaw-space-sm);
|
||||
}
|
||||
|
||||
.openclaw-lab-card-head > h4 {
|
||||
margin: 0;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.openclaw-lab-meta {
|
||||
color: var(--openclaw-color-fg-muted);
|
||||
font-size: var(--openclaw-font-sm);
|
||||
}
|
||||
|
||||
.openclaw-lab-config,
|
||||
.openclaw-lab-results {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--openclaw-space-sm);
|
||||
}
|
||||
|
||||
.openclaw-lab-dim-row {
|
||||
display: grid;
|
||||
grid-template-columns: 84px 110px 1fr 32px;
|
||||
gap: var(--openclaw-space-sm);
|
||||
align-items: end;
|
||||
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||
background: rgba(255, 255, 255, 0.02);
|
||||
border-radius: 6px;
|
||||
padding: var(--openclaw-space-sm);
|
||||
}
|
||||
|
||||
.openclaw-lab-dim-row .openclaw-form-group.wide {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.openclaw-lab-plan-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: var(--openclaw-space-sm);
|
||||
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||
border-radius: 6px;
|
||||
background: rgba(255, 255, 255, 0.02);
|
||||
padding: var(--openclaw-space-sm);
|
||||
}
|
||||
|
||||
.openclaw-lab-plan-header > h4 {
|
||||
margin: 0;
|
||||
font-size: var(--openclaw-font-md);
|
||||
}
|
||||
|
||||
.openclaw-lab-run-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.openclaw-lab-run-item {
|
||||
display: grid;
|
||||
grid-template-columns: 72px 1fr auto 32px;
|
||||
align-items: center;
|
||||
gap: var(--openclaw-space-sm);
|
||||
padding: 8px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||
border-radius: 6px;
|
||||
background: rgba(255, 255, 255, 0.02);
|
||||
}
|
||||
|
||||
.run-idx {
|
||||
font-family: var(--openclaw-font-mono);
|
||||
color: var(--openclaw-color-fg-muted);
|
||||
font-size: var(--openclaw-font-sm);
|
||||
}
|
||||
|
||||
.run-params {
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
font-family: var(--openclaw-font-mono);
|
||||
font-size: var(--openclaw-font-sm);
|
||||
}
|
||||
|
||||
.run-status {
|
||||
font-size: var(--openclaw-font-sm);
|
||||
border-radius: 999px;
|
||||
padding: 2px 8px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.15);
|
||||
color: var(--openclaw-color-fg-muted);
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.run-status.pending,
|
||||
.run-status.queued {
|
||||
color: #ffe38f;
|
||||
border-color: rgba(230, 180, 50, 0.55);
|
||||
background: rgba(230, 180, 50, 0.14);
|
||||
}
|
||||
|
||||
.run-status.running {
|
||||
color: #a8d1ff;
|
||||
border-color: rgba(90, 150, 255, 0.55);
|
||||
background: rgba(90, 150, 255, 0.16);
|
||||
}
|
||||
|
||||
.run-status.success,
|
||||
.run-status.completed {
|
||||
color: #bfffc0;
|
||||
border-color: rgba(42, 170, 42, 0.55);
|
||||
background: rgba(42, 170, 42, 0.16);
|
||||
}
|
||||
|
||||
.run-status.error,
|
||||
.run-status.failed {
|
||||
color: #ffb4b4;
|
||||
border-color: rgba(200, 60, 60, 0.6);
|
||||
background: rgba(200, 60, 60, 0.16);
|
||||
}
|
||||
|
||||
@media (max-width: 820px) {
|
||||
.openclaw-lab-header {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.openclaw-lab-actions {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.openclaw-lab-dim-row {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.openclaw-lab-run-item {
|
||||
grid-template-columns: 1fr;
|
||||
align-items: start;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 720px) {
|
||||
.openclaw-lab-actions {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,928 @@
|
||||
/*====================================
|
||||
* LEGACY COMPATIBILITY
|
||||
*====================================*/
|
||||
|
||||
/* Moltbot Design System */
|
||||
|
||||
:root {
|
||||
/* Spacing Scale */
|
||||
--moltbot-space-xs: 4px;
|
||||
--moltbot-space-sm: 8px;
|
||||
--moltbot-space-md: 12px;
|
||||
--moltbot-space-lg: 16px;
|
||||
--moltbot-space-xl: 24px;
|
||||
|
||||
/* Typography */
|
||||
--moltbot-font-xs: 11px;
|
||||
--moltbot-font-sm: 12px;
|
||||
--moltbot-font-md: 13px;
|
||||
--moltbot-font-lg: 14px;
|
||||
--moltbot-font-mono: 'Consolas', 'Monaco', 'Courier New', monospace;
|
||||
|
||||
/* Colors (inheriting from ComfyUI variable names where possible, fallbacks provided) */
|
||||
--moltbot-color-bg: var(--comfy-menu-bg, #222);
|
||||
--moltbot-color-bg-light: var(--comfy-input-bg, #333);
|
||||
--moltbot-color-fg: var(--fg-color, #eee);
|
||||
--moltbot-color-fg-muted: #aaa;
|
||||
--moltbot-color-border: var(--border-color, #444);
|
||||
--moltbot-color-primary: var(--primary-color, #2a2);
|
||||
--moltbot-color-danger: #d44;
|
||||
--moltbot-color-warning: #ea0;
|
||||
--moltbot-color-success: #2a2;
|
||||
}
|
||||
|
||||
/* Layout Primitives */
|
||||
|
||||
.moltbot-sidebar-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
min-width: 560px; /* Keep sidebar readable: header tabs + Parameter Lab controls */
|
||||
min-height: 0; /* allow children to shrink for overflow handling */
|
||||
background: var(--moltbot-color-bg);
|
||||
color: var(--moltbot-color-fg);
|
||||
font-family: Arial, sans-serif;
|
||||
font-size: var(--moltbot-font-md);
|
||||
}
|
||||
|
||||
/* CRITICAL: Keep this rule + !important; SplitterPanel inline sizing can override inner container min-width. */
|
||||
.side-bar-panel:has(.moltbot-sidebar-container) {
|
||||
min-width: 560px !important;
|
||||
}
|
||||
|
||||
.moltbot-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: var(--moltbot-space-md);
|
||||
padding: var(--moltbot-space-md) var(--moltbot-space-lg);
|
||||
border-bottom: 1px solid var(--moltbot-color-border);
|
||||
background: rgba(0,0,0,0.15);
|
||||
}
|
||||
|
||||
.moltbot-status-dot {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border-radius: 50%;
|
||||
background: #666;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
.moltbot-status-dot.ok { background: #2a2; }
|
||||
.moltbot-status-dot.warn { background: #ea0; }
|
||||
.moltbot-status-dot.err { background: #c44; }
|
||||
|
||||
.moltbot-title {
|
||||
font-size: var(--moltbot-font-lg);
|
||||
font-weight: 700;
|
||||
flex: 1 1 auto;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.moltbot-badges {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
gap: var(--moltbot-space-sm);
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.moltbot-version {
|
||||
font-size: var(--moltbot-font-sm);
|
||||
color: var(--moltbot-color-fg-muted);
|
||||
}
|
||||
|
||||
.moltbot-repo-link {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 4px 8px;
|
||||
border: 1px solid var(--moltbot-color-border);
|
||||
border-radius: 999px;
|
||||
background: rgba(255,255,255,0.04);
|
||||
color: var(--moltbot-color-fg);
|
||||
text-decoration: none;
|
||||
font-size: var(--moltbot-font-sm);
|
||||
white-space: nowrap;
|
||||
}
|
||||
.moltbot-repo-link:hover { background: rgba(255,255,255,0.08); }
|
||||
|
||||
.moltbot-banner {
|
||||
padding: var(--moltbot-space-sm) var(--moltbot-space-lg);
|
||||
border-bottom: 1px solid var(--moltbot-color-border);
|
||||
font-size: var(--moltbot-font-sm);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--moltbot-space-md);
|
||||
justify-content: space-between;
|
||||
}
|
||||
.moltbot-banner > span { flex: 1; }
|
||||
|
||||
.moltbot-banner-warning { background: rgba(255, 170, 0, 0.10); color: #ffd27a; }
|
||||
.moltbot-banner-info { background: rgba(0, 120, 255, 0.10); color: #a8d1ff; }
|
||||
.moltbot-banner-success { background: rgba(42, 170, 42, 0.15); color: #bfffc0; }
|
||||
.moltbot-banner-error { background: rgba(200, 50, 50, 0.15); color: #ffb4b4; }
|
||||
|
||||
.moltbot-banner-action {
|
||||
background: rgba(255,255,255,0.1);
|
||||
border: 1px solid rgba(255,255,255,0.2);
|
||||
color: inherit;
|
||||
padding: 2px 8px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: var(--moltbot-font-xs);
|
||||
text-transform: uppercase;
|
||||
font-weight: bold;
|
||||
}
|
||||
.moltbot-banner-action:hover { background: rgba(255,255,255,0.2); }
|
||||
|
||||
.moltbot-banner-close {
|
||||
background: none;
|
||||
border: none;
|
||||
color: inherit;
|
||||
opacity: 0.7;
|
||||
cursor: pointer;
|
||||
font-size: 16px;
|
||||
line-height: 1;
|
||||
padding: 0 4px;
|
||||
}
|
||||
.moltbot-banner-close:hover { opacity: 1; }
|
||||
|
||||
.moltbot-tabs {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr)); /* 4 tabs per row */
|
||||
grid-auto-flow: row; /* left-to-right, then top-to-bottom */
|
||||
gap: 6px;
|
||||
padding: 8px var(--moltbot-space-lg);
|
||||
border-bottom: 1px solid var(--moltbot-color-border);
|
||||
background: rgba(0,0,0,0.10);
|
||||
}
|
||||
|
||||
.moltbot-tab {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 6px 10px;
|
||||
border-radius: 6px;
|
||||
border: 1px solid transparent;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
color: var(--moltbot-color-fg-muted);
|
||||
background: rgba(255,255,255,0.02);
|
||||
font-size: var(--moltbot-font-sm);
|
||||
line-height: 1;
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.moltbot-tab:hover {
|
||||
color: var(--moltbot-color-fg);
|
||||
border-color: rgba(255,255,255,0.08);
|
||||
background: rgba(255,255,255,0.04);
|
||||
}
|
||||
|
||||
.moltbot-tab.active {
|
||||
color: var(--moltbot-color-fg);
|
||||
border-color: rgba(42, 170, 42, 0.5);
|
||||
background: rgba(42, 170, 42, 0.15);
|
||||
}
|
||||
|
||||
.moltbot-tab-icon {
|
||||
font-size: 13px;
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.moltbot-tab-label {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.moltbot-content {
|
||||
flex: 1 1 auto;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.moltbot-tab-pane {
|
||||
display: none;
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
}
|
||||
.moltbot-tab-pane.active {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.moltbot-panel {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
color: var(--moltbot-color-fg);
|
||||
font-size: var(--moltbot-font-md);
|
||||
background: var(--moltbot-color-bg);
|
||||
}
|
||||
|
||||
.moltbot-scroll-area {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: var(--moltbot-space-lg); /* More breathing room */
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--moltbot-space-lg);
|
||||
}
|
||||
|
||||
.moltbot-card {
|
||||
background: rgba(0, 0, 0, 0.2);
|
||||
border: 1px solid var(--moltbot-color-border);
|
||||
border-radius: 4px;
|
||||
padding: var(--moltbot-space-md);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--moltbot-space-sm);
|
||||
}
|
||||
|
||||
.moltbot-section-header {
|
||||
font-size: var(--moltbot-font-sm);
|
||||
text-transform: uppercase;
|
||||
color: var(--moltbot-color-fg-muted);
|
||||
letter-spacing: 0.05em;
|
||||
margin-bottom: var(--moltbot-space-xs);
|
||||
font-weight: bold;
|
||||
border-bottom: 1px solid var(--moltbot-color-border);
|
||||
padding-bottom: var(--moltbot-space-xs);
|
||||
}
|
||||
|
||||
/* Grids & Splits */
|
||||
.moltbot-grid-2 {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: var(--moltbot-space-md);
|
||||
}
|
||||
|
||||
.moltbot-split-v {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--moltbot-space-md);
|
||||
}
|
||||
|
||||
.moltbot-split-h {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: var(--moltbot-space-md);
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
/* Form Elements */
|
||||
.moltbot-input-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--moltbot-space-xs);
|
||||
}
|
||||
|
||||
.moltbot-section {
|
||||
background: rgba(0,0,0,0.18);
|
||||
border: 1px solid var(--moltbot-color-border);
|
||||
border-radius: 6px;
|
||||
padding: var(--moltbot-space-lg);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--moltbot-space-md);
|
||||
}
|
||||
|
||||
.moltbot-section > h4 {
|
||||
margin: 0;
|
||||
font-size: var(--moltbot-font-lg);
|
||||
}
|
||||
|
||||
.moltbot-form-row {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--moltbot-space-xs);
|
||||
}
|
||||
|
||||
.moltbot-btn-row {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: var(--moltbot-space-sm);
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.moltbot-note {
|
||||
padding: var(--moltbot-space-sm) var(--moltbot-space-md);
|
||||
border: 1px solid var(--moltbot-color-border);
|
||||
border-radius: 4px;
|
||||
background: rgba(255,255,255,0.03);
|
||||
color: var(--moltbot-color-fg-muted);
|
||||
font-size: var(--moltbot-font-sm);
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
.moltbot-status {
|
||||
font-size: var(--moltbot-font-sm);
|
||||
color: var(--moltbot-color-fg-muted);
|
||||
min-height: 16px;
|
||||
}
|
||||
.moltbot-status.ok { color: #bfffc0; }
|
||||
.moltbot-status.error { color: #ffb4b4; }
|
||||
|
||||
.moltbot-kv-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: var(--moltbot-space-md);
|
||||
}
|
||||
.moltbot-kv-key { color: var(--moltbot-color-fg-muted); }
|
||||
.moltbot-kv-val { color: var(--moltbot-color-fg); }
|
||||
.moltbot-kv-val.ok { color: #bfffc0; }
|
||||
.moltbot-kv-val.error { color: #ffb4b4; }
|
||||
|
||||
.moltbot-log-viewer {
|
||||
white-space: pre-wrap;
|
||||
font-family: var(--moltbot-font-mono);
|
||||
font-size: var(--moltbot-font-sm);
|
||||
background: rgba(0,0,0,0.25);
|
||||
border: 1px solid var(--moltbot-color-border);
|
||||
border-radius: 4px;
|
||||
padding: var(--moltbot-space-md);
|
||||
max-height: 260px;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.moltbot-checkbox {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.moltbot-help-btn {
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
border-radius: 999px;
|
||||
border: 1px solid var(--moltbot-color-border);
|
||||
background: rgba(255,255,255,0.04);
|
||||
color: var(--moltbot-color-fg);
|
||||
cursor: pointer;
|
||||
font-size: var(--moltbot-font-sm);
|
||||
line-height: 1;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
.moltbot-help-btn:hover {
|
||||
background: rgba(255,255,255,0.08);
|
||||
}
|
||||
|
||||
.moltbot-label {
|
||||
font-size: var(--moltbot-font-sm);
|
||||
color: var(--moltbot-color-fg-muted);
|
||||
}
|
||||
|
||||
.moltbot-input, .moltbot-select, .moltbot-textarea {
|
||||
background: var(--moltbot-color-bg-light);
|
||||
border: 1px solid var(--moltbot-color-border);
|
||||
color: var(--moltbot-color-fg);
|
||||
padding: 6px 8px;
|
||||
border-radius: 2px;
|
||||
font-size: var(--moltbot-font-md);
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.moltbot-input:focus, .moltbot-select:focus, .moltbot-textarea:focus {
|
||||
border-color: var(--moltbot-color-primary);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.moltbot-textarea {
|
||||
font-family: var(--moltbot-font-mono);
|
||||
resize: vertical;
|
||||
min-height: 60px;
|
||||
}
|
||||
|
||||
.moltbot-textarea-sm { min-height: 80px; }
|
||||
.moltbot-textarea-md { min-height: 150px; }
|
||||
.moltbot-textarea-lg { min-height: 250px; }
|
||||
.moltbot-textarea-xl { min-height: 400px; }
|
||||
|
||||
/* Buttons */
|
||||
.moltbot-btn {
|
||||
background: var(--moltbot-color-bg-light);
|
||||
border: 1px solid var(--moltbot-color-border);
|
||||
color: var(--moltbot-color-fg);
|
||||
padding: 6px 12px;
|
||||
cursor: pointer;
|
||||
font-size: var(--moltbot-font-md);
|
||||
border-radius: 2px;
|
||||
text-align: center;
|
||||
transition: background 0.1s;
|
||||
}
|
||||
|
||||
.moltbot-btn:hover:not(:disabled) {
|
||||
background: #444;
|
||||
}
|
||||
|
||||
.moltbot-btn:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.moltbot-btn-primary {
|
||||
background: #2a2; /* Green-ish */
|
||||
color: #fff;
|
||||
border-color: #181;
|
||||
}
|
||||
.moltbot-btn-primary:hover:not(:disabled) { background: #3b3; }
|
||||
|
||||
.moltbot-btn-danger {
|
||||
background: #a33;
|
||||
color: #fff;
|
||||
border-color: #822;
|
||||
}
|
||||
.moltbot-btn-danger:hover:not(:disabled) { background: #c44; }
|
||||
|
||||
.moltbot-btn-sm {
|
||||
padding: 2px 6px;
|
||||
font-size: var(--moltbot-font-sm);
|
||||
}
|
||||
|
||||
.moltbot-btn-success {
|
||||
background: rgba(42, 170, 42, 0.2);
|
||||
border-color: rgba(42, 170, 42, 0.6);
|
||||
color: #dfffe0;
|
||||
}
|
||||
|
||||
/* Compatibility aliases used by existing tabs */
|
||||
.moltbot-btn.primary {
|
||||
background: rgba(42, 170, 42, 0.2);
|
||||
border-color: rgba(42, 170, 42, 0.6);
|
||||
color: #dfffe0;
|
||||
}
|
||||
.moltbot-btn.primary:hover:not(:disabled) {
|
||||
background: rgba(42, 170, 42, 0.3);
|
||||
}
|
||||
.moltbot-btn.secondary {
|
||||
background: rgba(255, 255, 255, 0.04);
|
||||
border-color: var(--moltbot-color-border);
|
||||
color: var(--moltbot-color-fg);
|
||||
}
|
||||
|
||||
.moltbot-btn.has-icon {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.moltbot-btn-icon {
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
border: 1px solid var(--moltbot-color-border);
|
||||
background: rgba(255, 255, 255, 0.04);
|
||||
color: var(--moltbot-color-fg);
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: var(--moltbot-font-sm);
|
||||
line-height: 1;
|
||||
}
|
||||
.moltbot-btn-icon:hover {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.moltbot-separator {
|
||||
width: 1px;
|
||||
height: 24px;
|
||||
background: var(--moltbot-color-border);
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.moltbot-form-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.moltbot-form-group > label {
|
||||
font-size: var(--moltbot-font-sm);
|
||||
color: var(--moltbot-color-fg-muted);
|
||||
}
|
||||
|
||||
.moltbot-form-group > input,
|
||||
.moltbot-form-group > select,
|
||||
.moltbot-form-group > textarea {
|
||||
background: var(--moltbot-color-bg-light);
|
||||
border: 1px solid var(--moltbot-color-border);
|
||||
color: var(--moltbot-color-fg);
|
||||
padding: 6px 8px;
|
||||
border-radius: 4px;
|
||||
font-size: var(--moltbot-font-md);
|
||||
}
|
||||
.moltbot-form-group > input:focus,
|
||||
.moltbot-form-group > select:focus,
|
||||
.moltbot-form-group > textarea:focus {
|
||||
outline: none;
|
||||
border-color: rgba(42, 170, 42, 0.7);
|
||||
}
|
||||
|
||||
.moltbot-hint,
|
||||
.moltbot-loading,
|
||||
.moltbot-error {
|
||||
padding: 10px 12px;
|
||||
border-radius: 6px;
|
||||
font-size: var(--moltbot-font-sm);
|
||||
border: 1px solid var(--moltbot-color-border);
|
||||
background: rgba(255, 255, 255, 0.03);
|
||||
color: var(--moltbot-color-fg-muted);
|
||||
}
|
||||
|
||||
.moltbot-loading {
|
||||
color: #d2e4ff;
|
||||
border-color: rgba(90, 150, 255, 0.45);
|
||||
background: rgba(90, 150, 255, 0.12);
|
||||
}
|
||||
|
||||
.moltbot-error {
|
||||
color: #ffb4b4;
|
||||
border-color: rgba(200, 60, 60, 0.6);
|
||||
background: rgba(200, 60, 60, 0.18);
|
||||
}
|
||||
|
||||
/* Status Indicators */
|
||||
.moltbot-badge {
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
font-size: var(--moltbot-font-xs);
|
||||
font-weight: bold;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
.moltbot-badge.pending { background: #aa0; color: #000; }
|
||||
.moltbot-badge.approved { background: #2a2; color: #fff; }
|
||||
.moltbot-badge.rejected { background: #c00; color: #fff; }
|
||||
.moltbot-badge.expired { background: #666; color: #ccc; }
|
||||
|
||||
/* Error / Empty States */
|
||||
.moltbot-empty-state {
|
||||
padding: var(--moltbot-space-xl);
|
||||
text-align: center;
|
||||
color: var(--moltbot-color-fg-muted);
|
||||
border: 1px dashed var(--moltbot-color-border);
|
||||
border-radius: 4px;
|
||||
background: rgba(255, 255, 255, 0.02);
|
||||
}
|
||||
|
||||
.moltbot-error-box {
|
||||
padding: var(--moltbot-space-md);
|
||||
background: rgba(200, 50, 50, 0.1);
|
||||
border: 1px solid #a33;
|
||||
border-radius: 4px;
|
||||
color: #eaa;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--moltbot-space-sm);
|
||||
}
|
||||
|
||||
.moltbot-error-title {
|
||||
font-weight: bold;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--moltbot-space-sm);
|
||||
}
|
||||
|
||||
.moltbot-error-hint {
|
||||
font-size: var(--moltbot-font-sm);
|
||||
color: #ccc;
|
||||
background: rgba(0,0,0,0.3);
|
||||
padding: 4px;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
/* Loading Spinner (Simple) */
|
||||
.moltbot-spinner {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border: 2px solid rgba(255,255,255,0.3);
|
||||
border-radius: 50%;
|
||||
border-top-color: #fff;
|
||||
animation: spin 1s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
/* Modals */
|
||||
.moltbot-modal-overlay {
|
||||
position: fixed;
|
||||
top: 0; left: 0; right: 0; bottom: 0;
|
||||
background: rgba(0,0,0,0.7);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 9999;
|
||||
}
|
||||
|
||||
.moltbot-modal {
|
||||
background: var(--moltbot-color-bg);
|
||||
border: 1px solid var(--moltbot-color-border);
|
||||
box-shadow: 0 10px 25px rgba(0,0,0,0.5);
|
||||
border-radius: 6px;
|
||||
min-width: 400px;
|
||||
max-width: 90vw;
|
||||
max-height: 90vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.moltbot-modal-header {
|
||||
padding: var(--moltbot-space-md);
|
||||
border-bottom: 1px solid var(--moltbot-color-border);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
font-weight: bold;
|
||||
font-size: var(--moltbot-font-lg);
|
||||
}
|
||||
|
||||
.moltbot-modal-body {
|
||||
padding: var(--moltbot-space-lg);
|
||||
overflow-y: auto;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.moltbot-modal-footer {
|
||||
padding: var(--moltbot-space-md);
|
||||
border-top: 1px solid var(--moltbot-color-border);
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: var(--moltbot-space-md);
|
||||
background: rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
/* Parameter Lab */
|
||||
.moltbot-lab-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--moltbot-space-md);
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
padding: var(--moltbot-space-md);
|
||||
box-sizing: border-box;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.moltbot-lab-header {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--moltbot-space-md);
|
||||
border: 1px solid var(--moltbot-color-border);
|
||||
background: rgba(255, 255, 255, 0.03);
|
||||
border-radius: 8px;
|
||||
padding: var(--moltbot-space-md);
|
||||
}
|
||||
|
||||
.moltbot-lab-title-wrap {
|
||||
flex: 1 1 100%;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.moltbot-lab-title-wrap > h3 {
|
||||
margin: 0;
|
||||
font-size: 20px;
|
||||
line-height: 1.1;
|
||||
word-break: normal;
|
||||
overflow-wrap: normal;
|
||||
}
|
||||
|
||||
.moltbot-lab-title-wrap > p {
|
||||
margin: 4px 0 0;
|
||||
color: var(--moltbot-color-fg-muted);
|
||||
font-size: var(--moltbot-font-sm);
|
||||
line-height: 1.35;
|
||||
max-width: 60ch;
|
||||
word-break: normal;
|
||||
overflow-wrap: normal;
|
||||
}
|
||||
|
||||
.moltbot-lab-actions {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
gap: var(--moltbot-space-sm);
|
||||
width: 100%;
|
||||
margin: 0 auto;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.moltbot-lab-action-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 6px;
|
||||
min-width: 0;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.moltbot-lab-actions .moltbot-btn.has-icon.moltbot-lab-action-btn {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.moltbot-lab-action-icon {
|
||||
flex: 0 0 auto;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.moltbot-lab-action-label {
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.moltbot-lab-action-btn.active {
|
||||
border-color: rgba(42, 170, 42, 0.65);
|
||||
background: rgba(42, 170, 42, 0.2);
|
||||
color: #dfffe0;
|
||||
}
|
||||
|
||||
.moltbot-lab-action-btn:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.moltbot-lab-action-btn:focus-visible {
|
||||
border-color: rgba(42, 170, 42, 0.75);
|
||||
box-shadow: 0 0 0 1px rgba(42, 170, 42, 0.35) inset;
|
||||
}
|
||||
|
||||
.moltbot-lab-actions .moltbot-separator {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.moltbot-lab-main {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
gap: var(--moltbot-space-md);
|
||||
min-height: 0;
|
||||
overflow-y: auto;
|
||||
padding-right: 2px;
|
||||
}
|
||||
|
||||
.moltbot-lab-card {
|
||||
border: 1px solid var(--moltbot-color-border);
|
||||
background: rgba(0, 0, 0, 0.16);
|
||||
border-radius: 8px;
|
||||
padding: var(--moltbot-space-md);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--moltbot-space-md);
|
||||
}
|
||||
|
||||
.moltbot-lab-card-head {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
justify-content: space-between;
|
||||
gap: var(--moltbot-space-md);
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.08);
|
||||
padding-bottom: var(--moltbot-space-sm);
|
||||
}
|
||||
|
||||
.moltbot-lab-card-head > h4 {
|
||||
margin: 0;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.moltbot-lab-meta {
|
||||
color: var(--moltbot-color-fg-muted);
|
||||
font-size: var(--moltbot-font-sm);
|
||||
}
|
||||
|
||||
.moltbot-lab-config,
|
||||
.moltbot-lab-results {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--moltbot-space-sm);
|
||||
}
|
||||
|
||||
.moltbot-lab-dim-row {
|
||||
display: grid;
|
||||
grid-template-columns: 84px 110px 1fr 32px;
|
||||
gap: var(--moltbot-space-sm);
|
||||
align-items: end;
|
||||
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||
background: rgba(255, 255, 255, 0.02);
|
||||
border-radius: 6px;
|
||||
padding: var(--moltbot-space-sm);
|
||||
}
|
||||
|
||||
.moltbot-lab-dim-row .moltbot-form-group.wide {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.moltbot-lab-plan-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: var(--moltbot-space-sm);
|
||||
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||
border-radius: 6px;
|
||||
background: rgba(255, 255, 255, 0.02);
|
||||
padding: var(--moltbot-space-sm);
|
||||
}
|
||||
|
||||
.moltbot-lab-plan-header > h4 {
|
||||
margin: 0;
|
||||
font-size: var(--moltbot-font-md);
|
||||
}
|
||||
|
||||
.moltbot-lab-run-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.moltbot-lab-run-item {
|
||||
display: grid;
|
||||
grid-template-columns: 72px 1fr auto 32px;
|
||||
align-items: center;
|
||||
gap: var(--moltbot-space-sm);
|
||||
padding: 8px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||
border-radius: 6px;
|
||||
background: rgba(255, 255, 255, 0.02);
|
||||
}
|
||||
|
||||
.run-idx {
|
||||
font-family: var(--moltbot-font-mono);
|
||||
color: var(--moltbot-color-fg-muted);
|
||||
font-size: var(--moltbot-font-sm);
|
||||
}
|
||||
|
||||
.run-params {
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
font-family: var(--moltbot-font-mono);
|
||||
font-size: var(--moltbot-font-sm);
|
||||
}
|
||||
|
||||
.run-status {
|
||||
font-size: var(--moltbot-font-sm);
|
||||
border-radius: 999px;
|
||||
padding: 2px 8px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.15);
|
||||
color: var(--moltbot-color-fg-muted);
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.run-status.pending,
|
||||
.run-status.queued {
|
||||
color: #ffe38f;
|
||||
border-color: rgba(230, 180, 50, 0.55);
|
||||
background: rgba(230, 180, 50, 0.14);
|
||||
}
|
||||
|
||||
.run-status.running {
|
||||
color: #a8d1ff;
|
||||
border-color: rgba(90, 150, 255, 0.55);
|
||||
background: rgba(90, 150, 255, 0.16);
|
||||
}
|
||||
|
||||
.run-status.success,
|
||||
.run-status.completed {
|
||||
color: #bfffc0;
|
||||
border-color: rgba(42, 170, 42, 0.55);
|
||||
background: rgba(42, 170, 42, 0.16);
|
||||
}
|
||||
|
||||
.run-status.error,
|
||||
.run-status.failed {
|
||||
color: #ffb4b4;
|
||||
border-color: rgba(200, 60, 60, 0.6);
|
||||
background: rgba(200, 60, 60, 0.16);
|
||||
}
|
||||
|
||||
@media (max-width: 820px) {
|
||||
.moltbot-lab-header {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.moltbot-lab-actions {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.moltbot-lab-dim-row {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.moltbot-lab-run-item {
|
||||
grid-template-columns: 1fr;
|
||||
align-items: start;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 720px) {
|
||||
.moltbot-lab-actions {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
}
|
||||
+125
-151
@@ -1,152 +1,139 @@
|
||||
import { openclawApi } from "../openclaw_api.js";
|
||||
import { showError, clearError } from "../openclaw_utils.js";
|
||||
import {
|
||||
showError,
|
||||
clearError,
|
||||
normalizeLegacyClassNames,
|
||||
} from "../openclaw_utils.js";
|
||||
|
||||
// Helper for safe HTML escaping
|
||||
function escapeHtml(text) {
|
||||
if (!text) return "";
|
||||
return text
|
||||
.replace(/&/g, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">")
|
||||
.replace(/"/g, """)
|
||||
.replace(/\"/g, """)
|
||||
.replace(/'/g, "'");
|
||||
}
|
||||
|
||||
function stringifyVal(value) {
|
||||
if (typeof value === "object" && value !== null) return "{...}";
|
||||
return String(value);
|
||||
}
|
||||
|
||||
function renderInputsSummary(inputs) {
|
||||
if (!inputs) return "";
|
||||
const keys = Object.keys(inputs);
|
||||
if (keys.length === 0) return "No inputs";
|
||||
const preview = keys.slice(0, 2).map((key) => `${key}: ${stringifyVal(inputs[key])}`);
|
||||
if (keys.length > 2) preview.push(`+${keys.length - 2} more`);
|
||||
return escapeHtml(preview.join(", "));
|
||||
}
|
||||
|
||||
function renderApprovalItem(request) {
|
||||
const statusClass = request.status;
|
||||
const isPending = request.status === "pending";
|
||||
|
||||
return `
|
||||
<div class="openclaw-list-item" style="padding: 10px; border-bottom: 1px solid var(--openclaw-color-border); margin-bottom: 4px; border-left: 3px solid var(--openclaw-color-border);">
|
||||
<div style="display: flex; justify-content: space-between; align-items: flex-start; gap: 10px;">
|
||||
<div>
|
||||
<div style="font-weight: bold; font-size: var(--openclaw-font-md); color: var(--openclaw-color-fg);">
|
||||
${escapeHtml(request.template_id)}
|
||||
<span style="font-size: var(--openclaw-font-xs); font-weight: normal; color: var(--openclaw-color-fg-muted); margin-left:8px;">${escapeHtml(request.approval_id)}</span>
|
||||
</div>
|
||||
<div style="font-size: var(--openclaw-font-sm); color: var(--openclaw-color-fg-muted); margin-top: 4px;">
|
||||
Inputs: <span style="color: #aaa;">${renderInputsSummary(request.inputs)}</span>
|
||||
</div>
|
||||
<div style="font-size: var(--openclaw-font-xs); color: #666; margin-top: 4px;">
|
||||
Requested: ${new Date(request.requested_at).toLocaleString()}
|
||||
${request.source ? `via ${escapeHtml(request.source)}` : ""}
|
||||
</div>
|
||||
</div>
|
||||
<div style="display: flex; flex-direction: column; align-items: flex-end; gap: 5px;">
|
||||
<span class="openclaw-badge ${statusClass}">${request.status.toUpperCase()}</span>
|
||||
<div style="display: flex; gap: 5px; margin-top: 5px; flex-wrap: wrap; justify-content: flex-end;">
|
||||
<button class="openclaw-btn openclaw-btn-sm" data-action="details" data-id="${request.approval_id}">Details</button>
|
||||
${isPending ? `
|
||||
<button class="openclaw-btn openclaw-btn-sm openclaw-btn-primary" data-action="approve" data-id="${request.approval_id}">Approve</button>
|
||||
<button class="openclaw-btn openclaw-btn-sm openclaw-btn-danger" data-action="reject" data-id="${request.approval_id}">Reject</button>
|
||||
` : ""}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
export const ApprovalsTab = {
|
||||
id: "approvals",
|
||||
title: "Approvals",
|
||||
icon: "pi pi-check-circle",
|
||||
|
||||
render(container) {
|
||||
// --- 1. Static Layout ---
|
||||
container.innerHTML = `
|
||||
<div class="openclaw-panel openclaw-panel moltbot-panel">
|
||||
<div class="openclaw-card openclaw-card moltbot-card" style="border-radius:0; border:none; border-bottom:1px solid var(--moltbot-color-border);">
|
||||
<div class="openclaw-section-header openclaw-section-header moltbot-section-header">Approval Requests</div>
|
||||
<div class="openclaw-error-box openclaw-error-box moltbot-error-box" style="display:none"></div>
|
||||
<div class="openclaw-toolbar openclaw-toolbar moltbot-toolbar" style="margin-top:5px; display:flex; gap:5px; align-items:center;" id="apr-toolbar">
|
||||
<div id="apr-filter-btns" style="display: flex; gap: 5px;">
|
||||
<button class="openclaw-btn openclaw-btn moltbot-btn openclaw-btn-primary openclaw-btn-primary moltbot-btn-primary" data-status="pending">Pending</button>
|
||||
<button class="openclaw-btn openclaw-btn moltbot-btn" data-status="approved">Approved</button>
|
||||
<button class="openclaw-btn openclaw-btn moltbot-btn" data-status="rejected">Rejected</button>
|
||||
<button class="openclaw-btn openclaw-btn moltbot-btn" data-status="">All</button>
|
||||
<div class="openclaw-panel">
|
||||
<div class="openclaw-card" style="border-radius:0; border:none; border-bottom:1px solid var(--openclaw-color-border);">
|
||||
<div class="openclaw-section-header">Approval Requests</div>
|
||||
<div class="openclaw-error-box" style="display:none"></div>
|
||||
<div class="openclaw-toolbar" style="margin-top:5px; display:flex; gap:5px; align-items:center;" id="apr-toolbar">
|
||||
<div id="apr-filter-btns" style="display: flex; gap: 5px; flex-wrap: wrap;">
|
||||
<button class="openclaw-btn openclaw-btn-primary" data-status="pending">Pending</button>
|
||||
<button class="openclaw-btn" data-status="approved">Approved</button>
|
||||
<button class="openclaw-btn" data-status="rejected">Rejected</button>
|
||||
<button class="openclaw-btn" data-status="">All</button>
|
||||
</div>
|
||||
<button class="openclaw-btn openclaw-btn moltbot-btn openclaw-btn-sm openclaw-btn-sm moltbot-btn-sm" id="apr-refresh-btn" style="margin-left: auto;">
|
||||
Refresh
|
||||
</button>
|
||||
<button class="openclaw-btn openclaw-btn-sm" id="apr-refresh-btn" style="margin-left: auto;">Refresh</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="apr-list" class="openclaw-scroll-area openclaw-scroll-area moltbot-scroll-area" style="padding:0;">
|
||||
<div class="openclaw-empty-state openclaw-empty-state moltbot-empty-state">Loading...</div>
|
||||
<div id="apr-list" class="openclaw-scroll-area" style="padding:0;">
|
||||
<div class="openclaw-empty-state">Loading...</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Details Modal -->
|
||||
<div id="apr-editor-overlay" class="openclaw-modal-overlay openclaw-modal-overlay moltbot-modal-overlay" style="display:none;">
|
||||
<div id="apr-details-modal" class="openclaw-modal openclaw-modal moltbot-modal" style="width: 600px;">
|
||||
<div class="openclaw-modal-header openclaw-modal-header moltbot-modal-header">
|
||||
<div id="apr-editor-overlay" class="openclaw-modal-overlay" style="display:none;">
|
||||
<div id="apr-details-modal" class="openclaw-modal" style="width: 600px;">
|
||||
<div class="openclaw-modal-header">
|
||||
<span id="apr-modal-title">Request Details</span>
|
||||
</div>
|
||||
|
||||
<div class="openclaw-modal-body openclaw-modal-body moltbot-modal-body">
|
||||
<div style="font-family: var(--moltbot-font-mono); font-size: var(--moltbot-font-xs); background: #111; padding: 10px; border: 1px solid #333; height: 300px; overflow: auto; white-space: pre-wrap;" id="apr-modal-content"></div>
|
||||
<div class="openclaw-modal-body">
|
||||
<div style="font-family: var(--openclaw-font-mono); font-size: var(--openclaw-font-xs); background: #111; padding: 10px; border: 1px solid #333; height: 300px; overflow: auto; white-space: pre-wrap;" id="apr-modal-content"></div>
|
||||
</div>
|
||||
|
||||
<div class="openclaw-modal-footer openclaw-modal-footer moltbot-modal-footer">
|
||||
<button class="openclaw-btn openclaw-btn moltbot-btn" id="apr-modal-close">Close</button>
|
||||
<div class="openclaw-modal-footer">
|
||||
<button class="openclaw-btn" id="apr-modal-close">Close</button>
|
||||
<div id="apr-modal-actions" style="display:flex; gap:10px;"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
normalizeLegacyClassNames(container);
|
||||
|
||||
// --- 2. State & References ---
|
||||
const ui = {
|
||||
list: container.querySelector("#apr-list"),
|
||||
filters: container.querySelector("#apr-filter-btns"),
|
||||
refreshBtn: container.querySelector("#apr-refresh-btn"),
|
||||
modal: {
|
||||
overlay: container.querySelector("#apr-editor-overlay"),
|
||||
el: container.querySelector("#apr-details-modal"),
|
||||
title: container.querySelector("#apr-modal-title"),
|
||||
content: container.querySelector("#apr-modal-content"),
|
||||
close: container.querySelector("#apr-modal-close"),
|
||||
actions: container.querySelector("#apr-modal-actions"),
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
let currentState = {
|
||||
const currentState = {
|
||||
status: "pending",
|
||||
approvals: []
|
||||
};
|
||||
|
||||
// --- 3. View Logic ---
|
||||
|
||||
const renderInputsSummary = (inputs) => {
|
||||
if (!inputs) return "";
|
||||
const keys = Object.keys(inputs);
|
||||
if (keys.length === 0) return "No inputs";
|
||||
// Show first 2 inputs
|
||||
const firstTwo = keys.slice(0, 2).map(k => `${k}: ${stringifyVal(inputs[k])}`);
|
||||
if (keys.length > 2) firstTwo.push(`+${keys.length - 2} more`);
|
||||
return escapeHtml(firstTwo.join(", "));
|
||||
};
|
||||
|
||||
const stringifyVal = (v) => {
|
||||
if (typeof v === "object") return "{...}";
|
||||
return String(v);
|
||||
}
|
||||
|
||||
const renderListItem = (req) => {
|
||||
// Mapping status to CSS classes is cleaner than inline colors
|
||||
const statusClass = req.status; // pending, approved, rejected
|
||||
const isPending = req.status === "pending";
|
||||
|
||||
return `
|
||||
<div class="openclaw-list-item openclaw-list-item moltbot-list-item" style="padding: 10px; border-bottom: 1px solid var(--moltbot-color-border); margin-bottom: 4px; border-left: 3px solid var(--moltbot-color-border);">
|
||||
<div style="display: flex; justify-content: space-between; align-items: flex-start;">
|
||||
<div>
|
||||
<div style="font-weight: bold; font-size: var(--moltbot-font-md); color: var(--moltbot-color-fg);">
|
||||
${escapeHtml(req.template_id)}
|
||||
<span style="font-size: var(--moltbot-font-xs); font-weight: normal; color: var(--moltbot-color-fg-muted); margin-left:8px;">${escapeHtml(req.approval_id)}</span>
|
||||
</div>
|
||||
<div style="font-size: var(--moltbot-font-sm); color: var(--moltbot-color-fg-muted); margin-top: 4px;">
|
||||
Inputs: <span style="color: #aaa;">${renderInputsSummary(req.inputs)}</span>
|
||||
</div>
|
||||
<div style="font-size: var(--moltbot-font-xs); color: #666; margin-top: 4px;">
|
||||
Requested: ${new Date(req.requested_at).toLocaleString()}
|
||||
${req.source ? `via ${escapeHtml(req.source)}` : ''}
|
||||
</div>
|
||||
</div>
|
||||
<div style="display: flex; flex-direction: column; align-items: flex-end; gap: 5px;">
|
||||
<span class="openclaw-badge openclaw-badge moltbot-badge ${statusClass}">
|
||||
${req.status.toUpperCase()}
|
||||
</span>
|
||||
|
||||
<div style="display: flex; gap: 5px; margin-top: 5px;">
|
||||
<button class="openclaw-btn openclaw-btn moltbot-btn openclaw-btn-sm openclaw-btn-sm moltbot-btn-sm" data-action="details" data-id="${req.approval_id}">Details</button>
|
||||
${isPending ? `
|
||||
<button class="openclaw-btn openclaw-btn moltbot-btn openclaw-btn-sm openclaw-btn-sm moltbot-btn-sm openclaw-btn-primary openclaw-btn-primary moltbot-btn-primary" data-action="approve" data-id="${req.approval_id}">Approve</button>
|
||||
<button class="openclaw-btn openclaw-btn moltbot-btn openclaw-btn-sm openclaw-btn-sm moltbot-btn-sm openclaw-btn-danger openclaw-btn-danger moltbot-btn-danger" data-action="reject" data-id="${req.approval_id}">Reject</button>
|
||||
` : ''}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
approvals: [],
|
||||
};
|
||||
|
||||
const renderList = () => {
|
||||
if (currentState.approvals.length === 0) {
|
||||
ui.list.innerHTML = '<div class="openclaw-empty-state openclaw-empty-state moltbot-empty-state">No requests found.</div>';
|
||||
ui.list.innerHTML = '<div class="openclaw-empty-state">No requests found.</div>';
|
||||
return;
|
||||
}
|
||||
ui.list.innerHTML = currentState.approvals.map(renderListItem).join("");
|
||||
ui.list.innerHTML = currentState.approvals.map(renderApprovalItem).join("");
|
||||
normalizeLegacyClassNames(ui.list);
|
||||
};
|
||||
|
||||
// --- 4. Logic ---
|
||||
|
||||
const loadApprovals = async () => {
|
||||
clearError(container);
|
||||
ui.list.innerHTML = '<div style="padding: 10px; text-align: center;">Loading...</div>';
|
||||
@@ -159,7 +146,7 @@ export const ApprovalsTab = {
|
||||
currentState.approvals = res.data.approvals || [];
|
||||
renderList();
|
||||
} else {
|
||||
ui.list.innerHTML = ''; // Clear loading
|
||||
ui.list.innerHTML = "";
|
||||
showError(container, res.error);
|
||||
}
|
||||
};
|
||||
@@ -169,7 +156,7 @@ export const ApprovalsTab = {
|
||||
|
||||
const res = await openclawApi.approveRequest(id, { autoExecute: true });
|
||||
if (res.ok) {
|
||||
alert(`Approved! ` + (res.data.executed ? `Executed as prompt ${res.data.prompt_id}` : `Marked approved.`));
|
||||
alert(`Approved! ${res.data.executed ? `Executed as prompt ${res.data.prompt_id}` : "Marked approved."}`);
|
||||
loadApprovals();
|
||||
} else {
|
||||
showError(container, `Approval failed: ${res.error}`);
|
||||
@@ -187,69 +174,58 @@ export const ApprovalsTab = {
|
||||
}
|
||||
};
|
||||
|
||||
const showDetails = async (id) => {
|
||||
// Find in local state first, or fetch
|
||||
let req = currentState.approvals.find(a => a.approval_id === id);
|
||||
|
||||
if (!req) {
|
||||
const res = await openclawApi.getApproval(id);
|
||||
if (res.ok) req = res.data.approval;
|
||||
}
|
||||
|
||||
if (!req) return;
|
||||
|
||||
ui.modal.content.textContent = JSON.stringify(req, null, 2);
|
||||
|
||||
// Render actions
|
||||
if (req.status === "pending") {
|
||||
ui.modal.actions.innerHTML = `
|
||||
<button class="openclaw-btn openclaw-btn moltbot-btn openclaw-btn-primary openclaw-btn-primary moltbot-btn-primary" id="apr-modal-approve">Approve</button>
|
||||
<button class="openclaw-btn openclaw-btn moltbot-btn openclaw-btn-danger openclaw-btn-danger moltbot-btn-danger" id="apr-modal-reject">Reject</button>
|
||||
`;
|
||||
|
||||
// Bind dynamic buttons
|
||||
container.querySelector("#apr-modal-approve").onclick = () => { handleApprove(id); closeModal(); };
|
||||
container.querySelector("#apr-modal-reject").onclick = () => { handleReject(id); closeModal(); };
|
||||
|
||||
} else {
|
||||
ui.modal.actions.innerHTML = "";
|
||||
}
|
||||
|
||||
openModal();
|
||||
const closeModal = () => {
|
||||
ui.modal.overlay.style.display = "none";
|
||||
};
|
||||
|
||||
const openModal = () => {
|
||||
ui.modal.overlay.style.display = "flex";
|
||||
};
|
||||
|
||||
const closeModal = () => {
|
||||
ui.modal.overlay.style.display = "none";
|
||||
const showDetails = async (id) => {
|
||||
let request = currentState.approvals.find((approval) => approval.approval_id === id);
|
||||
if (!request) {
|
||||
const res = await openclawApi.getApproval(id);
|
||||
if (res.ok) request = res.data.approval;
|
||||
}
|
||||
if (!request) return;
|
||||
|
||||
ui.modal.content.textContent = JSON.stringify(request, null, 2);
|
||||
if (request.status === "pending") {
|
||||
ui.modal.actions.innerHTML = `
|
||||
<button class="openclaw-btn openclaw-btn-primary" id="apr-modal-approve">Approve</button>
|
||||
<button class="openclaw-btn openclaw-btn-danger" id="apr-modal-reject">Reject</button>
|
||||
`;
|
||||
container.querySelector("#apr-modal-approve").onclick = () => {
|
||||
handleApprove(id);
|
||||
closeModal();
|
||||
};
|
||||
container.querySelector("#apr-modal-reject").onclick = () => {
|
||||
handleReject(id);
|
||||
closeModal();
|
||||
};
|
||||
} else {
|
||||
ui.modal.actions.innerHTML = "";
|
||||
}
|
||||
openModal();
|
||||
};
|
||||
|
||||
// --- 5. Event Binding ---
|
||||
ui.filters.addEventListener("click", (event) => {
|
||||
const btn = event.target.closest("button[data-status]");
|
||||
if (!btn || btn.parentElement !== ui.filters) return;
|
||||
|
||||
// Filters
|
||||
ui.filters.addEventListener("click", (e) => {
|
||||
const btn = e.target.closest("button[data-status]");
|
||||
if (btn && btn.parentElement === ui.filters) { // Ensure strict match
|
||||
// Update active state
|
||||
ui.filters.querySelectorAll("button").forEach(b => b.classList.remove("openclaw-btn-primary", "openclaw-btn-primary", "moltbot-btn-primary"));
|
||||
btn.classList.add("openclaw-btn-primary", "openclaw-btn-primary", "moltbot-btn-primary");
|
||||
|
||||
// Update state
|
||||
currentState.status = btn.dataset.status; // "" for all
|
||||
ui.filters
|
||||
.querySelectorAll("button[data-status]")
|
||||
.forEach((button) => button.classList.remove("openclaw-btn-primary"));
|
||||
btn.classList.add("openclaw-btn-primary");
|
||||
currentState.status = btn.dataset.status;
|
||||
loadApprovals();
|
||||
}
|
||||
});
|
||||
|
||||
// Refresh
|
||||
ui.refreshBtn.addEventListener("click", loadApprovals);
|
||||
|
||||
// List Actions
|
||||
ui.list.addEventListener("click", (e) => {
|
||||
const btn = e.target.closest("button[data-action]");
|
||||
ui.list.addEventListener("click", (event) => {
|
||||
const btn = event.target.closest("button[data-action]");
|
||||
if (!btn) return;
|
||||
|
||||
const action = btn.dataset.action;
|
||||
const id = btn.dataset.id;
|
||||
|
||||
@@ -258,13 +234,11 @@ export const ApprovalsTab = {
|
||||
else if (action === "details") showDetails(id);
|
||||
});
|
||||
|
||||
// Modal
|
||||
ui.modal.close.addEventListener("click", closeModal);
|
||||
ui.modal.overlay.addEventListener("click", (e) => {
|
||||
if (e.target === ui.modal.overlay) closeModal();
|
||||
ui.modal.overlay.addEventListener("click", (event) => {
|
||||
if (event.target === ui.modal.overlay) closeModal();
|
||||
});
|
||||
|
||||
// Initial Load
|
||||
loadApprovals();
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
+215
-228
@@ -1,85 +1,140 @@
|
||||
import { openclawApi } from "../openclaw_api.js";
|
||||
import { tabManager } from "../openclaw_tabs.js";
|
||||
import { showError, clearError, parseJsonOrThrow } from "../openclaw_utils.js";
|
||||
import {
|
||||
showError,
|
||||
clearError,
|
||||
parseJsonOrThrow,
|
||||
normalizeLegacyClassNames,
|
||||
} from "../openclaw_utils.js";
|
||||
import {
|
||||
normalizeLibraryCategory,
|
||||
filterLibraryItems,
|
||||
getLibraryApplyTarget,
|
||||
} from "./library_tab_state.js";
|
||||
|
||||
// Helper for safe HTML escaping
|
||||
function escapeHtml(text) {
|
||||
if (!text) return "";
|
||||
return text
|
||||
.replace(/&/g, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">")
|
||||
.replace(/"/g, """)
|
||||
.replace(/\"/g, """)
|
||||
.replace(/'/g, "'");
|
||||
}
|
||||
|
||||
function renderApplyButtons(preset) {
|
||||
if (preset.category === "prompt") {
|
||||
return `
|
||||
<button class="openclaw-btn openclaw-btn-sm openclaw-btn-primary" data-action="apply" data-id="${preset.id}">Plan</button>
|
||||
<button class="openclaw-btn openclaw-btn-sm openclaw-btn-primary" data-action="apply-refiner" data-id="${preset.id}">Refine</button>
|
||||
`;
|
||||
}
|
||||
if (preset.category === "params") {
|
||||
return `<button class="openclaw-btn openclaw-btn-sm openclaw-btn-primary" data-action="apply" data-id="${preset.id}">Use</button>`;
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
function renderPresetItem(preset) {
|
||||
return `
|
||||
<div class="openclaw-list-item" style="padding: 10px; border-bottom: 1px solid var(--openclaw-color-border); display: flex; justify-content: space-between; align-items: center; gap: 10px;">
|
||||
<div>
|
||||
<div style="font-weight: bold;">${escapeHtml(preset.name)}</div>
|
||||
<div style="font-size: var(--openclaw-font-sm); color: var(--openclaw-color-fg-muted); margin-top:4px;">
|
||||
<span class="openclaw-badge" style="background:#555; color:#eee;">${escapeHtml(preset.category)}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div style="display: flex; gap: 5px; flex-wrap: wrap; justify-content: flex-end;">
|
||||
${renderApplyButtons(preset)}
|
||||
<button class="openclaw-btn openclaw-btn-sm" data-action="edit" data-id="${preset.id}">Edit</button>
|
||||
<button class="openclaw-btn openclaw-btn-sm openclaw-btn-danger" data-action="delete" data-id="${preset.id}">Del</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
function renderPackItem(pack) {
|
||||
return `
|
||||
<div class="openclaw-list-item" style="padding: 10px; border-bottom: 1px solid var(--openclaw-color-border); display: flex; justify-content: space-between; align-items: center; gap: 10px;">
|
||||
<div>
|
||||
<div style="font-weight: bold;">${escapeHtml(pack.name)} <span style="font-weight:normal; opacity:0.7">v${escapeHtml(pack.version)}</span></div>
|
||||
<div style="font-size: var(--openclaw-font-sm); color: var(--openclaw-color-fg-muted); margin-top:4px;">
|
||||
<span class="openclaw-badge" style="background:#2c4f7c; color:#eee;">${escapeHtml(pack.type)}</span>
|
||||
<span style="margin-left:6px;">by ${escapeHtml(pack.author)}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div style="display: flex; gap: 5px; flex-wrap: wrap; justify-content: flex-end;">
|
||||
<button class="openclaw-btn openclaw-btn-sm" data-action="export-pack" data-name="${pack.name}" data-ver="${pack.version}">Export</button>
|
||||
<button class="openclaw-btn openclaw-btn-sm openclaw-btn-danger" data-action="delete-pack" data-name="${pack.name}" data-ver="${pack.version}">Uninst</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
export const LibraryTab = {
|
||||
id: "library",
|
||||
title: "Library",
|
||||
icon: "pi pi-book",
|
||||
|
||||
render(container) {
|
||||
// --- 1. Static Layout ---
|
||||
container.innerHTML = `
|
||||
<div class="openclaw-panel openclaw-panel moltbot-panel">
|
||||
<div class="openclaw-card openclaw-card moltbot-card" style="border-radius:0; border:none; border-bottom:1px solid var(--moltbot-color-border);">
|
||||
<div class="openclaw-section-header openclaw-section-header moltbot-section-header">Asset Library</div>
|
||||
<div class="openclaw-error-box openclaw-error-box moltbot-error-box" style="display:none"></div>
|
||||
<div class="openclaw-input-group openclaw-input-group moltbot-input-group">
|
||||
<input type="text" id="lib-search" class="openclaw-input openclaw-input moltbot-input" placeholder="Search...">
|
||||
<div class="openclaw-panel">
|
||||
<div class="openclaw-card" style="border-radius:0; border:none; border-bottom:1px solid var(--openclaw-color-border);">
|
||||
<div class="openclaw-section-header">Asset Library</div>
|
||||
<div class="openclaw-error-box" style="display:none"></div>
|
||||
<div class="openclaw-input-group">
|
||||
<input type="text" id="lib-search" class="openclaw-input" placeholder="Search...">
|
||||
</div>
|
||||
<div class="openclaw-toolbar openclaw-toolbar moltbot-toolbar" style="margin-top:8px; display:flex; gap:5px;" id="lib-filter-btns">
|
||||
<button class="openclaw-btn openclaw-btn moltbot-btn openclaw-btn-primary openclaw-btn-primary moltbot-btn-primary" data-cat="all">All</button>
|
||||
<button class="openclaw-btn openclaw-btn moltbot-btn" data-cat="prompt">Prompts</button>
|
||||
<button class="openclaw-btn openclaw-btn moltbot-btn" data-cat="params">Params</button>
|
||||
<button class="openclaw-btn openclaw-btn moltbot-btn" data-cat="packs">Packs</button>
|
||||
<button class="openclaw-btn openclaw-btn moltbot-btn" id="lib-new-btn" style="margin-left: auto;">+ New</button>
|
||||
<div class="openclaw-toolbar" style="margin-top:8px; display:flex; gap:5px;" id="lib-filter-btns">
|
||||
<button class="openclaw-btn openclaw-btn-primary" data-cat="all">All</button>
|
||||
<button class="openclaw-btn" data-cat="prompt">Prompts</button>
|
||||
<button class="openclaw-btn" data-cat="params">Params</button>
|
||||
<button class="openclaw-btn" data-cat="packs">Packs</button>
|
||||
<button class="openclaw-btn" id="lib-new-btn" style="margin-left: auto;">+ New</button>
|
||||
</div>
|
||||
<input type="file" id="lib-pack-upload" accept=".zip" style="display:none">
|
||||
</div>
|
||||
|
||||
<div id="lib-list" class="openclaw-scroll-area openclaw-scroll-area moltbot-scroll-area" style="padding:0;">
|
||||
<!-- Items -->
|
||||
<div class="openclaw-empty-state openclaw-empty-state moltbot-empty-state">Loading...</div>
|
||||
<div id="lib-list" class="openclaw-scroll-area" style="padding:0;">
|
||||
<div class="openclaw-empty-state">Loading...</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Editor Modal (Presets) -->
|
||||
<div id="lib-editor-overlay" class="openclaw-modal-overlay openclaw-modal-overlay moltbot-modal-overlay" style="display:none;">
|
||||
<div id="lib-editor" class="openclaw-modal openclaw-modal moltbot-modal">
|
||||
<div class="openclaw-modal-header openclaw-modal-header moltbot-modal-header">
|
||||
<div id="lib-editor-overlay" class="openclaw-modal-overlay" style="display:none;">
|
||||
<div id="lib-editor" class="openclaw-modal">
|
||||
<div class="openclaw-modal-header">
|
||||
<span id="lib-editor-title">Edit Preset</span>
|
||||
<input type="hidden" id="lib-edit-id">
|
||||
</div>
|
||||
<div class="openclaw-modal-body openclaw-modal-body moltbot-modal-body">
|
||||
<div class="openclaw-input-group openclaw-input-group moltbot-input-group">
|
||||
<label class="openclaw-label openclaw-label moltbot-label">Name</label>
|
||||
<input type="text" id="lib-edit-name" class="openclaw-input openclaw-input moltbot-input">
|
||||
<div class="openclaw-modal-body">
|
||||
<div class="openclaw-input-group">
|
||||
<label class="openclaw-label">Name</label>
|
||||
<input type="text" id="lib-edit-name" class="openclaw-input">
|
||||
</div>
|
||||
<br>
|
||||
<div class="openclaw-input-group openclaw-input-group moltbot-input-group">
|
||||
<label class="openclaw-label openclaw-label moltbot-label">Category</label>
|
||||
<select id="lib-edit-cat" class="openclaw-select openclaw-select moltbot-select">
|
||||
<div class="openclaw-input-group">
|
||||
<label class="openclaw-label">Category</label>
|
||||
<select id="lib-edit-cat" class="openclaw-select">
|
||||
<option value="general">General</option>
|
||||
<option value="prompt">Prompt</option>
|
||||
<option value="params">Params</option>
|
||||
</select>
|
||||
</div>
|
||||
<br>
|
||||
<div class="openclaw-input-group openclaw-input-group moltbot-input-group">
|
||||
<label class="openclaw-label openclaw-label moltbot-label">Content (JSON)</label>
|
||||
<textarea id="lib-edit-params-json" class="openclaw-textarea openclaw-textarea moltbot-textarea openclaw-textarea-md openclaw-textarea-md moltbot-textarea-md"></textarea>
|
||||
<div class="openclaw-input-group">
|
||||
<label class="openclaw-label">Content (JSON)</label>
|
||||
<textarea id="lib-edit-params-json" class="openclaw-textarea openclaw-textarea-md"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div class="openclaw-modal-footer openclaw-modal-footer moltbot-modal-footer">
|
||||
<button class="openclaw-btn openclaw-btn moltbot-btn" id="lib-editor-cancel">Cancel</button>
|
||||
<button class="openclaw-btn openclaw-btn moltbot-btn openclaw-btn-primary openclaw-btn-primary moltbot-btn-primary" id="lib-editor-save">Save</button>
|
||||
<div class="openclaw-modal-footer">
|
||||
<button class="openclaw-btn" id="lib-editor-cancel">Cancel</button>
|
||||
<button class="openclaw-btn openclaw-btn-primary" id="lib-editor-save">Save</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
normalizeLegacyClassNames(container);
|
||||
|
||||
// --- 2. State & References ---
|
||||
const ui = {
|
||||
list: container.querySelector("#lib-list"),
|
||||
search: container.querySelector("#lib-search"),
|
||||
@@ -88,7 +143,6 @@ export const LibraryTab = {
|
||||
packUpload: container.querySelector("#lib-pack-upload"),
|
||||
modal: {
|
||||
overlay: container.querySelector("#lib-editor-overlay"),
|
||||
el: container.querySelector("#lib-editor"),
|
||||
title: container.querySelector("#lib-editor-title"),
|
||||
id: container.querySelector("#lib-edit-id"),
|
||||
name: container.querySelector("#lib-edit-name"),
|
||||
@@ -96,98 +150,41 @@ export const LibraryTab = {
|
||||
content: container.querySelector("#lib-edit-params-json"),
|
||||
save: container.querySelector("#lib-editor-save"),
|
||||
cancel: container.querySelector("#lib-editor-cancel"),
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
let currentState = {
|
||||
category: null, // 'packs' is a special category here
|
||||
items: []
|
||||
const currentState = {
|
||||
category: null,
|
||||
items: [],
|
||||
};
|
||||
|
||||
// --- 3. View Logic (Renderers) ---
|
||||
|
||||
const renderPresetItem = (p) => `
|
||||
<div class="openclaw-list-item openclaw-list-item moltbot-list-item" style="padding: 10px; border-bottom: 1px solid var(--moltbot-color-border); display: flex; justify-content: space-between; align-items: center;">
|
||||
<div>
|
||||
<div style="font-weight: bold;">${escapeHtml(p.name)}</div>
|
||||
<div style="font-size: var(--moltbot-font-sm); color: var(--moltbot-color-fg-muted); margin-top:4px;">
|
||||
<span class="openclaw-badge openclaw-badge moltbot-badge" style="background:#555; color:#eee;">${escapeHtml(p.category)}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div style="display: flex; gap: 5px;">
|
||||
${getApplyButton(p)}
|
||||
<button class="openclaw-btn openclaw-btn moltbot-btn openclaw-btn-sm openclaw-btn-sm moltbot-btn-sm" data-action="edit" data-id="${p.id}">Edit</button>
|
||||
<button class="openclaw-btn openclaw-btn moltbot-btn openclaw-btn-sm openclaw-btn-sm moltbot-btn-sm openclaw-btn-danger openclaw-btn-danger moltbot-btn-danger" data-action="delete" data-id="${p.id}">Del</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
const renderPackItem = (p) => `
|
||||
<div class="openclaw-list-item openclaw-list-item moltbot-list-item" style="padding: 10px; border-bottom: 1px solid var(--moltbot-color-border); display: flex; justify-content: space-between; align-items: center;">
|
||||
<div>
|
||||
<div style="font-weight: bold;">${escapeHtml(p.name)} <span style="font-weight:normal; opacity:0.7">v${escapeHtml(p.version)}</span></div>
|
||||
<div style="font-size: var(--moltbot-font-sm); color: var(--moltbot-color-fg-muted); margin-top:4px;">
|
||||
<span class="openclaw-badge openclaw-badge moltbot-badge" style="background:#2c4f7c; color:#eee;">${escapeHtml(p.type)}</span>
|
||||
<span style="margin-left:6px;">by ${escapeHtml(p.author)}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div style="display: flex; gap: 5px;">
|
||||
<button class="openclaw-btn openclaw-btn moltbot-btn openclaw-btn-sm openclaw-btn-sm moltbot-btn-sm" data-action="export-pack" data-name="${p.name}" data-ver="${p.version}">Export</button>
|
||||
<button class="openclaw-btn openclaw-btn moltbot-btn openclaw-btn-sm openclaw-btn-sm moltbot-btn-sm openclaw-btn-danger openclaw-btn-danger moltbot-btn-danger" data-action="delete-pack" data-name="${p.name}" data-ver="${p.version}">Uninst</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
function getApplyButton(p) {
|
||||
if (p.category === "prompt") {
|
||||
return `
|
||||
<button class="openclaw-btn openclaw-btn moltbot-btn openclaw-btn-sm openclaw-btn-sm moltbot-btn-sm openclaw-btn-primary openclaw-btn-primary moltbot-btn-primary" data-action="apply" data-id="${p.id}">Plan</button>
|
||||
<button class="openclaw-btn openclaw-btn moltbot-btn openclaw-btn-sm openclaw-btn-sm moltbot-btn-sm openclaw-btn-primary openclaw-btn-primary moltbot-btn-primary" data-action="apply-refiner" data-id="${p.id}">Refine</button>
|
||||
`;
|
||||
} else if (p.category === "params") {
|
||||
return `<button class="openclaw-btn openclaw-btn moltbot-btn openclaw-btn-sm openclaw-btn-sm moltbot-btn-sm openclaw-btn-primary openclaw-btn-primary moltbot-btn-primary" data-action="apply" data-id="${p.id}">Use</button>`;
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
const renderList = () => {
|
||||
const term = ui.search.value.toLowerCase();
|
||||
const filtered = currentState.items.filter(item =>
|
||||
item.name.toLowerCase().includes(term)
|
||||
);
|
||||
|
||||
const filtered = filterLibraryItems(currentState.items, ui.search.value);
|
||||
if (filtered.length === 0) {
|
||||
ui.list.innerHTML = '<div class="openclaw-empty-state openclaw-empty-state moltbot-empty-state">No items found.</div>';
|
||||
ui.list.innerHTML = '<div class="openclaw-empty-state">No items found.</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
if (currentState.category === "packs") {
|
||||
ui.list.innerHTML = filtered.map(renderPackItem).join("");
|
||||
} else {
|
||||
ui.list.innerHTML = filtered.map(renderPresetItem).join("");
|
||||
}
|
||||
ui.list.innerHTML = currentState.category === "packs"
|
||||
? filtered.map(renderPackItem).join("")
|
||||
: filtered.map(renderPresetItem).join("");
|
||||
normalizeLegacyClassNames(ui.list);
|
||||
};
|
||||
|
||||
// --- 4. Logic/Controllers ---
|
||||
|
||||
const loadContent = async () => {
|
||||
clearError(container);
|
||||
ui.list.innerHTML = '<div style="padding: 20px; text-align: center;">Loading...</div>';
|
||||
|
||||
let res;
|
||||
if (currentState.category === "packs") {
|
||||
res = await openclawApi.getPacks();
|
||||
} else {
|
||||
// If cat is 'all', allow backend/logic to handle null
|
||||
const cat = currentState.category === "all" ? null : currentState.category;
|
||||
res = await openclawApi.listPresets(cat);
|
||||
}
|
||||
const category = normalizeLibraryCategory(currentState.category);
|
||||
const res = category === "packs"
|
||||
? await openclawApi.getPacks()
|
||||
: await openclawApi.listPresets(category);
|
||||
|
||||
if (res.ok) {
|
||||
currentState.items = res.data || (res.packs ? res.packs : []);
|
||||
renderList();
|
||||
} else {
|
||||
ui.list.innerHTML = '';
|
||||
ui.list.innerHTML = "";
|
||||
showError(container, res.error);
|
||||
}
|
||||
};
|
||||
@@ -200,39 +197,44 @@ export const LibraryTab = {
|
||||
ui.modal.name.value = preset.name;
|
||||
ui.modal.cat.value = preset.category;
|
||||
ui.modal.content.value = JSON.stringify(preset.content, null, 2);
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
|
||||
ui.modal.title.textContent = "New Preset";
|
||||
ui.modal.id.value = "";
|
||||
ui.modal.name.value = "New Preset";
|
||||
ui.modal.cat.value = "general";
|
||||
ui.modal.content.value = "{}";
|
||||
}
|
||||
};
|
||||
|
||||
const closeModal = () => { ui.modal.overlay.style.display = "none"; };
|
||||
const closeModal = () => {
|
||||
ui.modal.overlay.style.display = "none";
|
||||
};
|
||||
|
||||
const savePreset = async () => {
|
||||
const id = ui.modal.id.value;
|
||||
const name = ui.modal.name.value.trim();
|
||||
const cat = ui.modal.cat.value;
|
||||
const category = ui.modal.cat.value;
|
||||
let content;
|
||||
|
||||
try {
|
||||
content = parseJsonOrThrow(
|
||||
ui.modal.content.value,
|
||||
"Content must be valid JSON"
|
||||
);
|
||||
content = parseJsonOrThrow(ui.modal.content.value, "Content must be valid JSON");
|
||||
} catch (error) {
|
||||
alert(error.message);
|
||||
return;
|
||||
}
|
||||
catch (e) { alert(e.message); return; }
|
||||
|
||||
if (!name) { alert("Name required"); return; }
|
||||
if (!name) {
|
||||
alert("Name required");
|
||||
return;
|
||||
}
|
||||
|
||||
ui.modal.save.textContent = "Saving...";
|
||||
ui.modal.save.disabled = true;
|
||||
|
||||
let res;
|
||||
if (id) res = await openclawApi.updatePreset(id, { name, category: cat, content });
|
||||
else res = await openclawApi.createPreset({ name, category: cat, content });
|
||||
const res = id
|
||||
? await openclawApi.updatePreset(id, { name, category, content })
|
||||
: await openclawApi.createPreset({ name, category, content });
|
||||
|
||||
ui.modal.save.textContent = "Save";
|
||||
ui.modal.save.disabled = false;
|
||||
@@ -248,113 +250,19 @@ export const LibraryTab = {
|
||||
const deleteItem = async (idOrName, version = null) => {
|
||||
if (!confirm("Are you sure you want to delete this item?")) return;
|
||||
|
||||
let res;
|
||||
if (currentState.category === "packs") {
|
||||
res = await openclawApi.deletePack(idOrName, version);
|
||||
} else {
|
||||
res = await openclawApi.deletePreset(idOrName);
|
||||
}
|
||||
const res = currentState.category === "packs"
|
||||
? await openclawApi.deletePack(idOrName, version)
|
||||
: await openclawApi.deletePreset(idOrName);
|
||||
|
||||
if (res.ok) loadContent();
|
||||
else alert(`Delete failed: ${res.error}`);
|
||||
};
|
||||
|
||||
// Pack Import
|
||||
ui.packUpload.onchange = async () => {
|
||||
const file = ui.packUpload.files[0];
|
||||
if (!file) return;
|
||||
|
||||
ui.list.innerHTML = '<div style="padding: 20px; text-align: center;">Importing Pack...</div>';
|
||||
|
||||
const res = await openclawApi.importPack(file, false); // No overwrite by default for now
|
||||
if (res.ok) {
|
||||
alert(`Imported ${res.data.pack.name} v${res.data.pack.version}`);
|
||||
loadContent();
|
||||
} else {
|
||||
showError(container, res.error);
|
||||
// Reload to restore list
|
||||
setTimeout(loadContent, 2000);
|
||||
}
|
||||
ui.packUpload.value = ""; // Reset
|
||||
};
|
||||
|
||||
const triggerNew = () => {
|
||||
if (currentState.category === "packs") {
|
||||
ui.packUpload.click();
|
||||
} else {
|
||||
openModal();
|
||||
alert(`Delete failed: ${res.error}`);
|
||||
}
|
||||
};
|
||||
|
||||
// --- 5. Event Binding ---
|
||||
|
||||
ui.search.addEventListener("input", renderList);
|
||||
|
||||
ui.filters.addEventListener("click", (e) => {
|
||||
const btn = e.target.closest("button[data-cat]");
|
||||
if (!btn) return;
|
||||
|
||||
ui.filters.querySelectorAll("button").forEach(b => b.classList.remove("openclaw-btn-primary", "openclaw-btn-primary", "moltbot-btn-primary"));
|
||||
btn.classList.add("openclaw-btn-primary", "openclaw-btn-primary", "moltbot-btn-primary");
|
||||
|
||||
const cat = btn.dataset.cat;
|
||||
currentState.category = cat === "all" ? null : cat;
|
||||
|
||||
// Update New Button Text
|
||||
ui.newBtn.textContent = currentState.category === "packs" ? "Import" : "+ New";
|
||||
|
||||
loadContent();
|
||||
});
|
||||
|
||||
ui.newBtn.addEventListener("click", triggerNew);
|
||||
|
||||
// List Delegation
|
||||
ui.list.addEventListener("click", async (e) => {
|
||||
const btn = e.target.closest("button[data-action]");
|
||||
if (!btn) return;
|
||||
const action = btn.dataset.action;
|
||||
|
||||
if (action === "edit") {
|
||||
const res = await openclawApi.getPreset(btn.dataset.id);
|
||||
if (res.ok) openModal(res.data);
|
||||
else showError(container, "Failed to load preset details");
|
||||
} else if (action === "delete") {
|
||||
await deleteItem(btn.dataset.id);
|
||||
} else if (action === "delete-pack") {
|
||||
await deleteItem(btn.dataset.name, btn.dataset.ver);
|
||||
} else if (action === "export-pack") {
|
||||
const res = await openclawApi.exportPack(btn.dataset.name, btn.dataset.ver);
|
||||
if (res.ok) {
|
||||
// Create blob link and click it
|
||||
const url = window.URL.createObjectURL(res.data);
|
||||
const a = document.createElement("a");
|
||||
a.href = url;
|
||||
a.download = `${btn.dataset.name}-${btn.dataset.ver}.zip`; // Or preserve filename from header if possible
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
a.remove();
|
||||
setTimeout(() => window.URL.revokeObjectURL(url), 1000);
|
||||
} else {
|
||||
showError(container, res.error);
|
||||
}
|
||||
} else if (action === "apply") {
|
||||
const res = await openclawApi.getPreset(btn.dataset.id);
|
||||
if (res.ok) applyPreset(res.data, "planner");
|
||||
else showError(container, "Failed to load preset for apply");
|
||||
} else if (action === "apply-refiner") {
|
||||
const res = await openclawApi.getPreset(btn.dataset.id);
|
||||
if (res.ok) applyPreset(res.data, "refiner");
|
||||
else showError(container, "Failed to load preset for apply");
|
||||
}
|
||||
});
|
||||
|
||||
// Apply Logic reused...
|
||||
function applyPreset(preset, explicitTarget = null) {
|
||||
let targetTabId = explicitTarget;
|
||||
if (!targetTabId) {
|
||||
if (preset.category === "prompt") targetTabId = "planner";
|
||||
else if (preset.category === "params") targetTabId = "variants";
|
||||
}
|
||||
const targetTabId = getLibraryApplyTarget(preset, explicitTarget);
|
||||
if (!targetTabId) return;
|
||||
|
||||
tabManager.activateTab(targetTabId);
|
||||
@@ -370,26 +278,105 @@ export const LibraryTab = {
|
||||
if (neg && content.negative) neg.value = content.negative;
|
||||
} else if (targetTabId === "variants") {
|
||||
const baseParams = document.getElementById("var-base-params");
|
||||
if (baseParams && content.params) baseParams.value = JSON.stringify(content.params, null, 2);
|
||||
if (baseParams && content.params) {
|
||||
baseParams.value = JSON.stringify(content.params, null, 2);
|
||||
}
|
||||
} else if (targetTabId === "refiner") {
|
||||
const pos = document.getElementById("refiner-orig-pos");
|
||||
const neg = document.getElementById("refiner-orig-neg");
|
||||
if (pos && content.positive) pos.value = content.positive;
|
||||
if (neg && content.negative) neg.value = content.negative;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Failed to apply preset:", e);
|
||||
} catch (error) {
|
||||
console.error("Failed to apply preset:", error);
|
||||
alert("Error applying preset to target tab.");
|
||||
}
|
||||
}, 100);
|
||||
}
|
||||
|
||||
ui.search.addEventListener("input", renderList);
|
||||
|
||||
ui.filters.addEventListener("click", (event) => {
|
||||
const btn = event.target.closest("button[data-cat]");
|
||||
if (!btn) return;
|
||||
|
||||
ui.filters
|
||||
.querySelectorAll("button[data-cat]")
|
||||
.forEach((button) => button.classList.remove("openclaw-btn-primary"));
|
||||
btn.classList.add("openclaw-btn-primary");
|
||||
|
||||
currentState.category = btn.dataset.cat || null;
|
||||
ui.newBtn.textContent = currentState.category === "packs" ? "Import" : "+ New";
|
||||
loadContent();
|
||||
});
|
||||
|
||||
ui.newBtn.addEventListener("click", () => {
|
||||
if (currentState.category === "packs") {
|
||||
ui.packUpload.click();
|
||||
return;
|
||||
}
|
||||
openModal();
|
||||
});
|
||||
|
||||
ui.packUpload.addEventListener("change", async () => {
|
||||
const file = ui.packUpload.files[0];
|
||||
if (!file) return;
|
||||
|
||||
ui.list.innerHTML = '<div style="padding: 20px; text-align: center;">Importing Pack...</div>';
|
||||
const res = await openclawApi.importPack(file, false);
|
||||
if (res.ok) {
|
||||
alert(`Imported ${res.data.pack.name} v${res.data.pack.version}`);
|
||||
loadContent();
|
||||
} else {
|
||||
showError(container, res.error);
|
||||
setTimeout(loadContent, 2000);
|
||||
}
|
||||
ui.packUpload.value = "";
|
||||
});
|
||||
|
||||
ui.list.addEventListener("click", async (event) => {
|
||||
const btn = event.target.closest("button[data-action]");
|
||||
if (!btn) return;
|
||||
const action = btn.dataset.action;
|
||||
|
||||
if (action === "edit") {
|
||||
const res = await openclawApi.getPreset(btn.dataset.id);
|
||||
if (res.ok) openModal(res.data);
|
||||
else showError(container, "Failed to load preset details");
|
||||
} else if (action === "delete") {
|
||||
await deleteItem(btn.dataset.id);
|
||||
} else if (action === "delete-pack") {
|
||||
await deleteItem(btn.dataset.name, btn.dataset.ver);
|
||||
} else if (action === "export-pack") {
|
||||
const res = await openclawApi.exportPack(btn.dataset.name, btn.dataset.ver);
|
||||
if (res.ok) {
|
||||
const url = window.URL.createObjectURL(res.data);
|
||||
const link = document.createElement("a");
|
||||
link.href = url;
|
||||
link.download = `${btn.dataset.name}-${btn.dataset.ver}.zip`;
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
link.remove();
|
||||
setTimeout(() => window.URL.revokeObjectURL(url), 1000);
|
||||
} else {
|
||||
showError(container, res.error);
|
||||
}
|
||||
} else if (action === "apply" || action === "apply-refiner") {
|
||||
const res = await openclawApi.getPreset(btn.dataset.id);
|
||||
if (res.ok) {
|
||||
applyPreset(res.data, action === "apply-refiner" ? "refiner" : null);
|
||||
} else {
|
||||
showError(container, "Failed to load preset for apply");
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
ui.modal.cancel.addEventListener("click", closeModal);
|
||||
ui.modal.save.addEventListener("click", savePreset);
|
||||
ui.modal.overlay.addEventListener("click", (e) => {
|
||||
if (e.target === ui.modal.overlay) closeModal();
|
||||
ui.modal.overlay.addEventListener("click", (event) => {
|
||||
if (event.target === ui.modal.overlay) closeModal();
|
||||
});
|
||||
|
||||
loadContent();
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
export function normalizeLibraryCategory(category) {
|
||||
if (!category || category === "all") return null;
|
||||
return category;
|
||||
}
|
||||
|
||||
export function filterLibraryItems(items = [], term = "") {
|
||||
const normalizedTerm = String(term || "").trim().toLowerCase();
|
||||
if (!normalizedTerm) {
|
||||
return Array.isArray(items) ? [...items] : [];
|
||||
}
|
||||
return (Array.isArray(items) ? items : []).filter((item) =>
|
||||
String(item?.name || "").toLowerCase().includes(normalizedTerm)
|
||||
);
|
||||
}
|
||||
|
||||
export function getLibraryApplyTarget(preset = {}, explicitTarget = null) {
|
||||
if (explicitTarget) return explicitTarget;
|
||||
if (preset?.category === "prompt") return "planner";
|
||||
if (preset?.category === "params") return "variants";
|
||||
return null;
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
normalizeLibraryCategory,
|
||||
filterLibraryItems,
|
||||
getLibraryApplyTarget,
|
||||
} from "../../tabs/library_tab_state.js";
|
||||
|
||||
describe("library_tab_state", () => {
|
||||
it("normalizes all-like categories to null", () => {
|
||||
expect(normalizeLibraryCategory(null)).toBeNull();
|
||||
expect(normalizeLibraryCategory("all")).toBeNull();
|
||||
expect(normalizeLibraryCategory("packs")).toBe("packs");
|
||||
});
|
||||
|
||||
it("filters items by case-insensitive name matches", () => {
|
||||
const items = [
|
||||
{ name: "Portrait Prompt" },
|
||||
{ name: "Landscape Params" },
|
||||
{ name: "Workflow Pack" },
|
||||
];
|
||||
|
||||
expect(filterLibraryItems(items, "prompt")).toEqual([{ name: "Portrait Prompt" }]);
|
||||
expect(filterLibraryItems(items, "PaR")).toEqual([{ name: "Landscape Params" }]);
|
||||
expect(filterLibraryItems(items, "")).toEqual(items);
|
||||
});
|
||||
|
||||
it("resolves apply targets from explicit target or category", () => {
|
||||
expect(getLibraryApplyTarget({ category: "prompt" })).toBe("planner");
|
||||
expect(getLibraryApplyTarget({ category: "params" })).toBe("variants");
|
||||
expect(getLibraryApplyTarget({ category: "general" }, "refiner")).toBe("refiner");
|
||||
expect(getLibraryApplyTarget({ category: "general" })).toBeNull();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,73 @@
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import {
|
||||
composeFetchWrappersOnce,
|
||||
getFetchWrapperMeta,
|
||||
withAbortPassthrough,
|
||||
withGetRetry,
|
||||
withPreconnectHint,
|
||||
} from "../../openclaw_fetch_wrappers.js";
|
||||
|
||||
describe("openclaw_fetch_wrappers", () => {
|
||||
beforeEach(() => {
|
||||
document.head.innerHTML = "";
|
||||
});
|
||||
|
||||
it("does not stack the same decorator chain twice", async () => {
|
||||
const fetchFn = vi.fn(async () => ({ ok: true }));
|
||||
const decorators = [withAbortPassthrough(), withGetRetry({ retries: 1 })];
|
||||
|
||||
const wrapped = composeFetchWrappersOnce(fetchFn, decorators);
|
||||
const wrappedAgain = composeFetchWrappersOnce(wrapped, decorators);
|
||||
|
||||
expect(wrappedAgain).toBe(wrapped);
|
||||
expect(getFetchWrapperMeta(wrappedAgain)).toMatchObject({
|
||||
baseFetch: fetchFn,
|
||||
appliedCount: 1,
|
||||
chainIds: ["abort_passthrough", "retry_get_1"],
|
||||
});
|
||||
|
||||
await wrappedAgain("/health");
|
||||
expect(fetchFn).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("adds one preconnect link per origin", async () => {
|
||||
const fetchFn = vi.fn(async () => ({ ok: true }));
|
||||
const wrapped = composeFetchWrappersOnce(fetchFn, [withPreconnectHint()]);
|
||||
|
||||
await wrapped("https://example.com/api/one");
|
||||
await wrapped("https://example.com/api/two");
|
||||
|
||||
const links = [...document.head.querySelectorAll('link[rel="preconnect"]')];
|
||||
expect(links).toHaveLength(1);
|
||||
expect(links[0].href).toContain("https://example.com");
|
||||
});
|
||||
|
||||
it("retries GET once on network failure", async () => {
|
||||
const fetchFn = vi
|
||||
.fn()
|
||||
.mockRejectedValueOnce(new Error("temporary"))
|
||||
.mockResolvedValueOnce({ ok: true, status: 200 });
|
||||
const wrapped = composeFetchWrappersOnce(fetchFn, [withGetRetry({ retries: 1 })]);
|
||||
|
||||
const result = await wrapped("/health", { method: "GET" });
|
||||
|
||||
expect(result).toMatchObject({ ok: true, status: 200 });
|
||||
expect(fetchFn).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it("does not retry non-GET requests", async () => {
|
||||
const fetchFn = vi.fn().mockRejectedValue(new Error("no retry"));
|
||||
const wrapped = composeFetchWrappersOnce(fetchFn, [withGetRetry({ retries: 2 })]);
|
||||
|
||||
await expect(wrapped("/config", { method: "POST" })).rejects.toThrow("no retry");
|
||||
expect(fetchFn).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("does not retry abort errors", async () => {
|
||||
const fetchFn = vi.fn().mockRejectedValue(new DOMException("Cancelled", "AbortError"));
|
||||
const wrapped = composeFetchWrappersOnce(fetchFn, [withGetRetry({ retries: 2 })]);
|
||||
|
||||
await expect(wrapped("/health")).rejects.toThrow(/Cancelled/);
|
||||
expect(fetchFn).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,52 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
makeEl,
|
||||
normalizeLegacyClassTokens,
|
||||
normalizeLegacyClassNames,
|
||||
parseJsonSafe,
|
||||
parseJsonOrThrow,
|
||||
isAbortError,
|
||||
} from "../../openclaw_utils.js";
|
||||
|
||||
describe("openclaw_utils", () => {
|
||||
it("creates elements with class and text", () => {
|
||||
const el = makeEl("div", "openclaw-card", "Hello");
|
||||
expect(el.tagName).toBe("DIV");
|
||||
expect(el.className).toBe("openclaw-card");
|
||||
expect(el.textContent).toBe("Hello");
|
||||
});
|
||||
|
||||
it("normalizes duplicate legacy class tokens", () => {
|
||||
expect(
|
||||
normalizeLegacyClassTokens("openclaw-btn moltbot-btn openclaw-btn-primary moltbot-btn-primary openclaw-btn")
|
||||
).toBe("openclaw-btn openclaw-btn-primary");
|
||||
});
|
||||
|
||||
it("normalizes class names in a subtree", () => {
|
||||
document.body.innerHTML = `
|
||||
<section class="openclaw-panel moltbot-panel">
|
||||
<button class="openclaw-btn moltbot-btn openclaw-btn-primary moltbot-btn-primary">Run</button>
|
||||
</section>
|
||||
`;
|
||||
const root = document.body.firstElementChild;
|
||||
normalizeLegacyClassNames(root);
|
||||
expect(root.className).toBe("openclaw-panel");
|
||||
expect(root.querySelector("button").className).toBe("openclaw-btn openclaw-btn-primary");
|
||||
});
|
||||
|
||||
it("returns fallback data for invalid JSON", () => {
|
||||
const parsed = parseJsonSafe("{bad", { safe: true });
|
||||
expect(parsed.ok).toBe(false);
|
||||
expect(parsed.value).toEqual({ safe: true });
|
||||
expect(parsed.error).toBeInstanceOf(Error);
|
||||
});
|
||||
|
||||
it("throws with the provided parse message", () => {
|
||||
expect(() => parseJsonOrThrow("{bad", "Broken payload")).toThrow(/Broken payload/);
|
||||
});
|
||||
|
||||
it("detects abort errors by name", () => {
|
||||
expect(isAbortError(new DOMException("Cancelled", "AbortError"))).toBe(true);
|
||||
expect(isAbortError(new Error("boom"))).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,13 @@
|
||||
import { vi } from "vitest";
|
||||
|
||||
if (!Element.prototype.scrollIntoView) {
|
||||
Element.prototype.scrollIntoView = vi.fn();
|
||||
}
|
||||
|
||||
if (!globalThis.alert) {
|
||||
globalThis.alert = vi.fn();
|
||||
}
|
||||
|
||||
if (!globalThis.confirm) {
|
||||
globalThis.confirm = vi.fn(() => true);
|
||||
}
|
||||
Reference in New Issue
Block a user