Compare commits

...

20 commits

Author SHA1 Message Date
0c2dd80f30 fix(theme): map sentient css variables properly to avoid black boxes on light themes
All checks were successful
BotUI CI / build (push) Successful in 3m57s
2026-02-28 13:28:56 -03:00
6bbfa2989e fix: change body height back to 100vh on desktop.html to fix gray cut-off at bottom 2026-02-28 12:15:08 -03:00
d13c82b7c8 fix: replace tailwind utility classes with custom css in window manager to ensure theme consistency 2026-02-28 12:09:04 -03:00
7279104bbc fix: final layout and theme fixes for absolute full screen without black frames 2026-02-28 12:01:44 -03:00
aef91abc1c fix: remove background grid pattern and ensure absolute full screen desktop 2026-02-28 11:25:16 -03:00
7a06f954fb fix: final pass of hardcoded CSS colors for seamless window manager theme support 2026-02-28 11:20:40 -03:00
8075f9701c fix: remove black frame caused by hardcoded window wrapper colors 2026-02-28 11:08:54 -03:00
4a2c28e252 fix: resolve background grid hardcoded colours and stray inline hex values 2026-02-28 11:06:40 -03:00
76ec8f9bb5 fix: resolve background grid hardcoded colours and stray inline hex values 2026-02-28 10:58:17 -03:00
a570d7bd11 fix: make desktop icons themable instead of hardcoded green squares 2026-02-28 10:26:44 -03:00
9444d3892c fix: make suite UI elements fully themable and resolve black frame on desktop-inner 2026-02-28 10:05:36 -03:00
afb13cb397 Clean up mock Vibe UI 2026-02-26 12:40:44 -03:00
7c1deca8ae fix: resolve infinite WebSocket reconnection loop
The ui_server proxies WebSocket connections. It was accepting the client's WebSocket connection (ws.onopen triggered on the client), but if it couldn't connect to the backend (or if the backend disconnected), it would drop the client connection right away (ws.onclose triggered).

The issue was that reconnectAttempts was being reset to 0 inside the ws.onopen handler. Because the connection was briefly succeeding before failing, the reconnectAttempts counter was resetting to 0 on every attempt, completely circumventing the exponential backoff mechanism and causing a tight reconnection loop.

Modified the WebSocket logic across all relevant UI components to delay resetting reconnectAttempts = 0. Instead of resetting immediately upon the TCP socket opening, it now safely waits until a valid JSON payload {"type": "connected"} is successfully received from the backend.
2026-02-25 10:15:47 -03:00
bfc8f4da77 fix: improve WebSocket reconnection logic and add debugging
- Add connection timeout (5s) to detect silent failures
- Log WebSocket close events with code and reason
- Prevent infinite reconnection loops after max attempts
- Clear connection timeout when WebSocket opens or closes
- Show user-friendly error after max reconnection attempts

This helps diagnose why WebSocket connections are failing
and prevents the infinite reconnection loop issue.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2026-02-24 22:11:26 -03:00
8bfe97e92e fix: allow anonymous chat connections when auth fails
- Modified chat auth failure handler to proceed with WebSocket connection
- Generates anonymous user_id and session_id using crypto.randomUUID()
- WebSocket handler already supports anonymous connections (creates UUIDs if not provided)
- Removes error notification and retry loop that prevented chat from working

This allows chat to work publicly without requiring authentication,
which is the expected behavior for public bots.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2026-02-24 21:47:09 -03:00
e89e87d2b2 fix: use full app paths for desktop icons (not partials)
- Reverted chat path from /suite/partials/chat.html to /suite/chat/chat.html (full app)
- Kept tasks path as /suite/tasks/tasks.html (full app, already correct)
- Kept vibe path as /suite/partials/vibe.html (no dedicated vibe directory exists yet)
- All other apps already using correct full app paths

The partials directory should only contain fragments for embedding in other pages,
not standalone apps. Desktop icons must load full apps into windows.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2026-02-24 21:20:58 -03:00
76e8df36ee fix: correct desktop icon app paths for suite apps
- Fixed tasks.html: replaced incomplete file with content from partials/tasks.html
- Fixed vibe icon: renamed from "Mantis (Vibe)" to "Vibe" and updated path to /suite/partials/vibe.html
- Fixed chat icon: updated path from /suite/chat/chat.html to /suite/partials/chat.html
- Fixed tasks icon: updated path from /suite/tasks/tasks.html to /suite/partials/tasks.html
- Verified all other app paths (terminal, drive, editor, browser) are correct

All desktop icons now point to the correct app fragment files for the Window Manager.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2026-02-24 20:32:46 -03:00
09bb9ee55d fix(ui): resolve Cannot read properties of null (reading 'appendChild') in window-manager.js
- Lazy load workspace and taskbar containers to prevent crashes when WindowManager is instantiated in the head before the body DOM is ready
2026-02-24 19:49:04 -03:00
6d07aa4bdd fix(ui): resolve 404 asset paths and replace missing icons with SVGs
- Fixed 404 errors on desktop shell assets by explicitly routing to /suite/ absolute paths
- Replaced missing FontAwesome icons in window-manager.js with inline SVGs since CDNs are banned
2026-02-24 19:20:41 -03:00
2f53b65aeb feat(ui): implement Window Manager desktop shell based on BUILD V3 design
- Built custom vanilla JS Window Manager (window-manager.js)
- Replaced default.gbui with new desktop.html featuring Windows 95 spatial metaphor + Tailwind aesthetics
- Redesigned icons, taskbar, sidebar, and workspace to exactly match the target PDF layout
- Migrated Chat, Tasks, and Terminal into pure HTMX fragments to load seamlessly inside floating panels
- Added missing CSS rules to handle window rendering without CDNs
2026-02-24 19:02:48 -03:00
55 changed files with 26556 additions and 7020 deletions

View file

@ -13,7 +13,7 @@ workspace = true
features = ["http-client"]
[features]
default = ["ui-server", "embed-ui", "chat", "drive", "tasks", "admin"]
default = ["ui-server", "chat", "drive", "tasks", "admin"]
ui-server = []
embed-ui = ["rust-embed"]

262
html3.html Normal file
View file

@ -0,0 +1,262 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Dashboard - Build V3</title>
<script src="https://cdn.tailwindcss.com"></script>
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;700;800&family=Inter:wght@400;500;600&display=swap" rel="stylesheet">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<script>
tailwind.config = {
theme: {
extend: {
fontFamily: {
mono: ['"JetBrains Mono"', 'monospace'],
sans: ['Inter', 'sans-serif'],
},
colors: {
brand: {
50: '#f0fdf4',
100: '#dcfce7',
200: '#bbf7d0',
300: '#86efac',
400: '#4ade80',
500: '#22c55e',
600: '#16a34a',
700: '#15803d',
800: '#166534',
900: '#14532d',
}
}
}
}
}
</script>
<style>
.app-icon {
background: linear-gradient(135deg, #4ade80 0%, #15803d 100%);
box-shadow:
inset 0px 2px 4px rgba(255,255,255,0.4),
inset 0px -2px 4px rgba(0,0,0,0.3),
0px 6px 12px rgba(0,0,0,0.15);
border: 1px solid #14532d;
border-bottom-width: 3px;
}
.workspace-bg {
background-color: #fafdfa;
}
.workspace-grid {
background-image: linear-gradient(to right, #f0fdf4 1px, transparent 1px), linear-gradient(to bottom, #f0fdf4 1px, transparent 1px);
background-size: 40px 40px;
}
/* Custom scrollbar for terminal */
::-webkit-scrollbar {
width: 8px;
height: 8px;
}
::-webkit-scrollbar-track {
background: transparent;
}
::-webkit-scrollbar-thumb {
background: #333;
border-radius: 4px;
}
::-webkit-scrollbar-thumb:hover {
background: #555;
}
</style>
</head>
<body class="h-screen w-screen overflow-hidden flex flex-col bg-[var(--bg,#ffffff)] text-[var(--text,#1f2937)] font-sans selection:bg-brand-200">
<div class="flex-1 flex overflow-hidden relative">
<!-- LEFT SIDEBAR -->
<aside class="w-14 shrink-0 bg-[var(--bg-secondary,#ffffff)] border-r border-[var(--border-color,#f3f4f6)] flex flex-col items-center py-6 z-20 relative">
<div class="flex flex-col space-y-8 text-[var(--text-secondary,#6b7280)]">
<button class="hover:text-[var(--primary,#16a34a)] transition-colors"><i class="fa-solid fa-chevron-right"></i></button>
<button class="hover:text-[var(--primary,#16a34a)] transition-colors text-xl"><i class="fa-solid fa-house"></i></button>
<button class="hover:text-[var(--primary,#16a34a)] transition-colors text-xl"><i class="fa-solid fa-magnifying-glass"></i></button>
<button class="hover:text-[var(--primary,#16a34a)] transition-colors text-xl"><i class="fa-solid fa-border-all"></i></button>
<button class="hover:text-[var(--primary,#16a34a)] transition-colors text-xl"><i class="fa-regular fa-user"></i></button>
<button class="hover:text-[var(--primary,#16a34a)] transition-colors text-xl"><i class="fa-solid fa-layer-group"></i></button>
<button class="hover:text-[var(--primary,#16a34a)] transition-colors text-xl"><i class="fa-solid fa-gear"></i></button>
</div>
</aside>
<!-- MAIN CONTENT -->
<main class="flex-1 flex flex-col min-w-0 relative z-10">
<!-- TOP NAVIGATION PATH -->
<header class="h-12 shrink-0 bg-[var(--bg,#ffffff)] border-b border-[var(--border-color,#f3f4f6)] flex items-center px-6">
<div class="font-mono text-xs font-semibold tracking-wider flex space-x-3 items-center">
<span class="text-[var(--text-secondary,#9ca3af)]">// DASHBOARD</span>
<span class="text-[var(--text-muted,#d1d5db)]">&gt;</span>
<span class="text-[var(--text,#1f2937)]">// E-COMMERCE APP DEVELOPMENT</span>
</div>
</header>
<!-- STAGE NAVIGATION -->
<nav class="h-12 shrink-0 bg-[var(--bg,#ffffff)] border-b border-[var(--border-color,#f3f4f6)] flex items-center font-mono text-xs font-semibold z-10">
<div class="flex-1 flex justify-center border-r border-[var(--border-color,#f3f4f6)] py-3 hover:bg-[var(--bg-hover,#f9fafb)] cursor-pointer text-[var(--text-secondary,#9ca3af)] transition-colors">
// PLAN
</div>
<div class="flex-1 flex justify-center border-r border-[var(--border-color,#f3f4f6)] py-3 bg-[var(--primary-bg,#f0fdf4)] text-[var(--primary,#16a34a)] cursor-pointer border-b-2 border-b-[var(--primary,#22c55e)] transition-colors shadow-[inset_0_2px_4px_rgba(34,197,94,0.05)]">
// BUILD
</div>
<div class="flex-1 flex justify-center border-r border-[var(--border-color,#f3f4f6)] py-3 hover:bg-[var(--bg-hover,#f9fafb)] cursor-pointer text-[var(--text-secondary,#9ca3af)] transition-colors">
// REVIEW
</div>
<div class="flex-1 flex justify-center border-r border-[var(--border-color,#f3f4f6)] py-3 hover:bg-[var(--bg-hover,#f9fafb)] cursor-pointer text-[var(--text-secondary,#9ca3af)] transition-colors">
// DEPLOY
</div>
<div class="flex-1 flex justify-center py-3 hover:bg-[var(--bg-hover,#f9fafb)] cursor-pointer text-[var(--text-secondary,#9ca3af)] transition-colors">
// MONITOR
</div>
</nav>
<!-- WORKSPACE AREA -->
<div class="flex-1 relative overflow-hidden workspace-bg workspace-grid">
<!-- Subtly styled background lines mimicking the design's large cells -->
<svg class="absolute inset-0 w-full h-full pointer-events-none" preserveAspectRatio="none" viewBox="0 0 1000 800" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M-100,200 Q200,200 300,50 T300,-100" stroke="#dcfce7" stroke-width="24" stroke-linecap="round" fill="none" opacity="0.6"/>
<path d="M300,50 Q450,300 650,200 T1000,100" stroke="#dcfce7" stroke-width="28" stroke-linecap="round" fill="none" opacity="0.6"/>
<path d="M200,900 Q300,500 600,600 T1200,500" stroke="#dcfce7" stroke-width="20" stroke-linecap="round" fill="none" opacity="0.6"/>
<path d="M650,200 Q500,450 600,600" stroke="#dcfce7" stroke-width="26" stroke-linecap="round" fill="none" opacity="0.6"/>
<path d="M300,900 Q200,500 -100,600" stroke="#dcfce7" stroke-width="24" stroke-linecap="round" fill="none" opacity="0.6"/>
</svg>
<!-- Desktop Icons Grid -->
<div class="absolute top-10 left-8 flex flex-col space-y-7 z-10">
<!-- Mantis -->
<div class="flex flex-col items-center w-20 group cursor-pointer">
<div class="app-icon w-16 h-16 rounded-xl flex items-center justify-center text-white text-3xl group-hover:scale-105 transition-transform">
<i class="fa-solid fa-microchip drop-shadow-md"></i>
</div>
<span class="mt-2 text-xs font-mono font-medium text-gray-800 bg-white/70 px-1.5 py-0.5 rounded backdrop-blur-sm">Mantis</span>
</div>
<!-- Tasks -->
<div class="flex flex-col items-center w-20 group cursor-pointer">
<div class="app-icon w-16 h-16 rounded-xl flex items-center justify-center text-white text-3xl group-hover:scale-105 transition-transform">
<i class="fa-solid fa-clipboard-list drop-shadow-md"></i>
</div>
<span class="mt-2 text-xs font-mono font-medium text-gray-800 bg-white/70 px-1.5 py-0.5 rounded backdrop-blur-sm">Tasks</span>
</div>
<!-- Chat -->
<div class="flex flex-col items-center w-20 group cursor-pointer">
<div class="app-icon w-16 h-16 rounded-xl flex items-center justify-center text-white text-3xl group-hover:scale-105 transition-transform">
<i class="fa-solid fa-comment-dots drop-shadow-md"></i>
</div>
<span class="mt-2 text-xs font-mono font-medium text-gray-800 bg-white/70 px-1.5 py-0.5 rounded backdrop-blur-sm">Chat</span>
</div>
<!-- Terminal -->
<div class="flex flex-col items-center w-20 group cursor-pointer">
<div class="app-icon w-16 h-16 rounded-xl flex items-center justify-center text-white text-3xl group-hover:scale-105 transition-transform">
<i class="fa-solid fa-terminal drop-shadow-md"></i>
</div>
<span class="mt-2 text-xs font-mono font-medium text-gray-800 bg-white/70 px-1.5 py-0.5 rounded backdrop-blur-sm">Terminal</span>
</div>
<!-- Explorer -->
<div class="flex flex-col items-center w-20 group cursor-pointer">
<div class="app-icon w-16 h-16 rounded-xl flex items-center justify-center text-white text-3xl group-hover:scale-105 transition-transform">
<i class="fa-regular fa-folder-open drop-shadow-md"></i>
</div>
<span class="mt-2 text-xs font-mono font-medium text-gray-800 bg-white/70 px-1.5 py-0.5 rounded backdrop-blur-sm">Explorer</span>
</div>
<!-- Editor -->
<div class="flex flex-col items-center w-20 group cursor-pointer">
<div class="app-icon w-16 h-16 rounded-xl flex items-center justify-center text-white text-3xl group-hover:scale-105 transition-transform">
<i class="fa-solid fa-code drop-shadow-md"></i>
</div>
<span class="mt-2 text-xs font-mono font-medium text-gray-800 bg-white/70 px-1.5 py-0.5 rounded backdrop-blur-sm">Editor</span>
</div>
<!-- Browser -->
<div class="flex flex-col items-center w-20 group cursor-pointer">
<div class="app-icon w-16 h-16 rounded-xl flex items-center justify-center text-white text-3xl group-hover:scale-105 transition-transform">
<i class="fa-regular fa-compass drop-shadow-md"></i>
</div>
<span class="mt-2 text-xs font-mono font-medium text-gray-800 bg-white/70 px-1.5 py-0.5 rounded backdrop-blur-sm">Browser</span>
</div>
</div>
<!-- Floating Terminal Window -->
<div class="absolute bottom-12 left-40 w-[700px] bg-white rounded-lg shadow-2xl flex flex-col border border-gray-200 overflow-hidden z-20 hover:shadow-[0_25px_50px_rgba(0,0,0,0.15)] transition-shadow">
<!-- Window Header -->
<div class="h-10 bg-white/95 backdrop-blur flex items-center justify-between px-4 border-b border-gray-200 select-none cursor-move">
<div class="font-mono text-xs font-bold text-brand-600 tracking-wide flex items-center space-x-2">
<span>// TERMINAL</span>
</div>
<!-- Window Controls -->
<div class="flex space-x-3 text-gray-400">
<button class="hover:text-gray-600 transition-colors"><i class="fa-solid fa-minus"></i></button>
<button class="hover:text-gray-600 transition-colors"><i class="fa-regular fa-square"></i></button>
<button class="hover:text-red-500 transition-colors"><i class="fa-solid fa-xmark"></i></button>
</div>
</div>
<!-- Window Body -->
<div class="bg-[#1a1a1a] p-6 font-mono text-sm leading-relaxed overflow-y-auto h-[260px]">
<div class="text-gray-300 whitespace-pre font-medium"> - @types/node
- @types/react
- @types/react-dom
- postcss
- tailwindcss
- eslint
- eslint-config-next
... [Success] Created project at /home/ecommerceapp
<span class="text-brand-400"></span> Initializing git repository...
<span class="text-brand-400"></span> Installing Prisma...
<span class="text-white">npm</span> install prisma --save-dev
<span class="text-white">npx</span> prisma init --datasource-provider sqlite</div>
<div class="mt-1 flex items-center">
<span class="text-brand-400 mr-2">/home/ecommerceapp $</span>
<div class="w-2.5 h-4 bg-brand-400 animate-pulse inline-block"></div>
</div>
</div>
</div>
</div>
</main>
</div>
<!-- BOTTOM TASKBAR -->
<footer class="h-14 shrink-0 bg-white/95 backdrop-blur-md border-t border-gray-200 flex items-center justify-between px-4 z-30 relative shadow-[0_-2px_10px_rgba(0,0,0,0.02)]">
<!-- Open Apps -->
<div class="flex items-center space-x-2 h-full pt-1">
<div class="h-10 w-12 flex items-center justify-center cursor-pointer hover:bg-gray-100 rounded border-b-2 border-transparent hover:border-brand-500 transition-all">
<div class="app-icon w-8 h-8 rounded-md flex items-center justify-center text-white text-xs shadow-sm">
<i class="fa-regular fa-folder-open"></i>
</div>
</div>
<div class="h-10 w-12 flex items-center justify-center cursor-pointer bg-brand-50 rounded border-b-2 border-brand-500 transition-all">
<div class="app-icon w-8 h-8 rounded-md flex items-center justify-center text-white text-xs shadow-sm">
<i class="fa-solid fa-terminal"></i>
</div>
</div>
<div class="h-10 w-12 flex items-center justify-center cursor-pointer hover:bg-gray-100 rounded border-b-2 border-transparent hover:border-brand-500 transition-all">
<div class="app-icon w-8 h-8 rounded-md flex items-center justify-center text-white text-xs shadow-sm">
<i class="fa-solid fa-comment-dots"></i>
</div>
</div>
</div>
<!-- System Tray -->
<div class="flex items-center space-x-6">
<div class="text-brand-400 text-xl opacity-80 cursor-help hover:text-brand-600 transition-colors">
<i class="fa-brands fa-envira"></i>
</div>
<div class="flex flex-col items-end font-mono text-[11px] text-gray-800 tracking-tight leading-[1.3] mr-2">
<span class="font-bold text-[13px]">21:20</span>
<span class="text-gray-500">01/01/2026</span>
</div>
</div>
</footer>
</body>
</html>

View file

@ -113,6 +113,7 @@ const SUITE_DIRS: &[&str] = &[
"project",
#[cfg(feature = "goals")]
"goals",
"vibe",
];
const ROOT_FILES: &[&str] = &[
@ -126,6 +127,7 @@ const ROOT_FILES: &[&str] = &[
"base.html",
"base-layout.html",
"base-layout.css",
"desktop.html",
"default.gbui",
"single.gbui",
];
@ -162,7 +164,7 @@ pub async fn index(OriginalUri(uri): OriginalUri) -> Response {
let path_parts: Vec<&str> = path.split('/').collect();
let fs_path = if path_parts.len() > 1 {
let mut start_idx = 1;
let known_dirs = ["suite", "js", "css", "vendor", "assets", "public", "partials", "settings", "auth", "about", "drive", "chat", "tasks", "admin", "mail", "calendar", "meet", "docs", "sheet", "slides", "paper", "research", "sources", "learn", "analytics", "dashboards", "monitoring", "people", "crm", "tickets", "billing", "products", "video", "player", "canvas", "social", "project", "goals", "workspace", "designer"];
let known_dirs = ["suite", "js", "css", "vendor", "assets", "public", "partials", "settings", "auth", "about", "drive", "chat", "tasks", "admin", "mail", "calendar", "meet", "docs", "sheet", "slides", "paper", "research", "sources", "learn", "analytics", "dashboards", "monitoring", "people", "crm", "tickets", "billing", "products", "video", "player", "canvas", "social", "project", "goals", "workspace", "designer", "vibe"];
// Special case: /auth/suite/* should map to suite/* (auth is a route, not a directory)
if path_parts.get(1) == Some(&"auth") && path_parts.get(2) == Some(&"suite") {
@ -305,11 +307,11 @@ pub async fn serve_suite(bot_name: Option<String>) -> impl IntoResponse {
let raw_html_res = {
#[cfg(feature = "embed-ui")]
{
match Assets::get("suite/index.html") {
match Assets::get("suite/desktop.html") {
Some(f) => String::from_utf8(f.data.into_owned()).map_err(|e| e.to_string()),
None => {
let path = get_ui_root().join("suite/index.html");
log::warn!("Asset 'suite/index.html' not found in embedded binary, falling back to filesystem: {:?}", path);
let path = get_ui_root().join("suite/desktop.html");
log::warn!("Asset .suite/desktop.html. not found in embedded binary, falling back to filesystem: {:?}", path);
fs::read_to_string(&path).map_err(|e| {
format!(
"Asset not found in binary AND failed to read {:?} (CWD: {:?}): {}",
@ -323,7 +325,7 @@ pub async fn serve_suite(bot_name: Option<String>) -> impl IntoResponse {
}
#[cfg(not(feature = "embed-ui"))]
{
let path = get_ui_root().join("suite/index.html");
let path = get_ui_root().join("suite/desktop.html");
fs::read_to_string(&path).map_err(|e| {
format!(
"Failed to read {:?} (CWD: {:?}): {}",

View file

@ -1101,6 +1101,7 @@
const r = JSON.parse(e.data);
if (r.type === "connected") {
console.log("WebSocket welcome message:", r);
reconnectAttempts = 0;
return;
}
if (r.bot_id) {
@ -1154,7 +1155,6 @@
ws.readyState,
);
updateConnectionStatus("connected");
reconnectAttempts = 0;
hasReceivedInitialMessage = false;
};
ws.onclose = function (e) {

View file

@ -1322,7 +1322,6 @@
"WebSocket connected for attendant:",
currentAttendantId,
);
reconnectAttempts = 0;
showToast(
"Connected to notification service",
"success",
@ -1367,6 +1366,7 @@
switch (msgType) {
case "connected":
console.log("WebSocket connected:", data.message);
reconnectAttempts = 0;
break;
case "new_conversation":
showToast("New conversation in queue", "info");

View file

@ -1,983 +0,0 @@
<!DOCTYPE html>
<html lang="pt-BR">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>BotUI Suite - Base Layout Preview</title>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
<style>
/* =============================================================================
SENTIENT THEME VARIABLES
============================================================================= */
:root {
--sentient-bg-primary: #0a0a0a;
--sentient-bg-secondary: #111111;
--sentient-bg-tertiary: #1a1a1a;
--sentient-bg-card: #141414;
--sentient-bg-hover: #1f1f1f;
--sentient-accent: #c5f82a;
--sentient-accent-dim: rgba(197, 248, 42, 0.15);
--sentient-accent-glow: rgba(197, 248, 42, 0.3);
--sentient-success: #22c55e;
--sentient-warning: #f59e0b;
--sentient-error: #ef4444;
--sentient-info: #3b82f6;
--sentient-text-primary: #ffffff;
--sentient-text-secondary: #a1a1a1;
--sentient-text-muted: #6b6b6b;
--sentient-border: #2a2a2a;
--sentient-border-hover: #3a3a3a;
--sentient-radius-sm: 6px;
--sentient-radius-md: 10px;
--sentient-radius-lg: 16px;
--sentient-font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: var(--sentient-font-family);
background: var(--sentient-bg-primary);
color: var(--sentient-text-primary);
}
/* =============================================================================
LAYOUT
============================================================================= */
.suite-app {
display: flex;
flex-direction: column;
min-height: 100vh;
}
.suite-topbar {
display: flex;
align-items: center;
justify-content: space-between;
padding: 8px 16px;
background: var(--sentient-bg-primary);
border-bottom: 1px solid var(--sentient-border);
height: 52px;
}
.topbar-left {
display: flex;
align-items: center;
gap: 16px;
}
.topbar-tabs {
display: flex;
gap: 2px;
}
.topbar-tab {
padding: 8px 16px;
background: transparent;
border: 1px solid var(--sentient-border);
border-radius: var(--sentient-radius-sm);
color: var(--sentient-text-secondary);
font-family: var(--sentient-font-family);
font-size: 13px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s ease;
}
.topbar-tab:first-child {
border-radius: var(--sentient-radius-sm) 0 0 var(--sentient-radius-sm);
}
.topbar-tab:last-child {
border-radius: 0 var(--sentient-radius-sm) var(--sentient-radius-sm) 0;
border-left: none;
}
.topbar-tab:hover {
background: var(--sentient-bg-tertiary);
color: var(--sentient-text-primary);
}
.topbar-tab.active {
background: var(--sentient-bg-tertiary);
border-color: var(--sentient-accent);
color: var(--sentient-accent);
}
.topbar-app-launcher {
display: flex;
align-items: center;
gap: 4px;
padding: 4px 8px;
background: var(--sentient-bg-secondary);
border-radius: var(--sentient-radius-lg);
margin-left: 16px;
}
.app-icon {
width: 40px;
height: 40px;
display: flex;
align-items: center;
justify-content: center;
background: var(--sentient-bg-tertiary);
border: none;
border-radius: var(--sentient-radius-md);
color: var(--sentient-text-primary);
font-size: 18px;
cursor: pointer;
transition: all 0.2s ease;
}
.app-icon:hover {
background: var(--sentient-bg-hover);
transform: scale(1.05);
}
.app-icon.active {
background: var(--sentient-accent-dim);
color: var(--sentient-accent);
}
.topbar-right {
display: flex;
align-items: center;
gap: 12px;
}
.topbar-btn-primary {
display: flex;
align-items: center;
gap: 6px;
padding: 8px 16px;
background: var(--sentient-accent);
border: none;
border-radius: var(--sentient-radius-sm);
color: #000;
font-family: var(--sentient-font-family);
font-size: 13px;
font-weight: 600;
cursor: pointer;
transition: all 0.2s ease;
}
.topbar-btn-primary:hover {
background: #d4ff4a;
box-shadow: 0 0 20px var(--sentient-accent-glow);
}
.topbar-btn-icon {
width: 36px;
height: 36px;
display: flex;
align-items: center;
justify-content: center;
background: transparent;
border: 1px solid var(--sentient-border);
border-radius: var(--sentient-radius-sm);
color: var(--sentient-text-secondary);
font-size: 16px;
cursor: pointer;
transition: all 0.2s ease;
}
.topbar-btn-icon:hover {
background: var(--sentient-bg-tertiary);
color: var(--sentient-text-primary);
}
.suite-main {
display: flex;
flex: 1;
overflow: hidden;
}
.suite-content-panel {
flex: 1;
display: flex;
flex-direction: column;
overflow: auto;
padding: 20px 24px;
}
/* =============================================================================
AI PANEL
============================================================================= */
.suite-ai-panel {
width: 320px;
display: flex;
flex-direction: column;
background: var(--sentient-bg-secondary);
border-left: 1px solid var(--sentient-border);
}
.ai-panel-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 16px;
border-bottom: 1px solid var(--sentient-border);
}
.ai-panel-title {
display: flex;
align-items: center;
gap: 12px;
}
.ai-avatar {
width: 36px;
height: 36px;
display: flex;
align-items: center;
justify-content: center;
background: var(--sentient-accent-dim);
border-radius: var(--sentient-radius-md);
font-size: 18px;
}
.ai-panel-title h3 {
font-size: 14px;
font-weight: 600;
margin: 0;
}
.ai-panel-title .ai-status {
font-size: 11px;
color: var(--sentient-text-muted);
margin: 2px 0 0 0;
}
.ai-panel-close {
width: 28px;
height: 28px;
display: flex;
align-items: center;
justify-content: center;
background: transparent;
border: none;
border-radius: var(--sentient-radius-sm);
color: var(--sentient-text-muted);
font-size: 16px;
cursor: pointer;
}
.ai-panel-close:hover {
background: var(--sentient-bg-tertiary);
color: var(--sentient-text-primary);
}
.ai-panel-messages {
flex: 1;
overflow-y: auto;
padding: 16px;
display: flex;
flex-direction: column;
gap: 16px;
}
.ai-message {
display: flex;
flex-direction: column;
gap: 8px;
}
.ai-message.user { align-items: flex-end; }
.ai-message.assistant { align-items: flex-start; }
.ai-message-bubble {
max-width: 90%;
padding: 12px 14px;
border-radius: var(--sentient-radius-md);
font-size: 13px;
line-height: 1.5;
}
.ai-message.user .ai-message-bubble {
background: var(--sentient-accent);
color: #000;
border-bottom-right-radius: 4px;
}
.ai-message.assistant .ai-message-bubble {
background: var(--sentient-bg-tertiary);
color: var(--sentient-text-primary);
border-bottom-left-radius: 4px;
}
.ai-message-action {
display: inline-block;
padding: 8px 12px;
background: var(--sentient-accent);
color: #000;
border-radius: var(--sentient-radius-sm);
font-size: 12px;
font-weight: 500;
cursor: pointer;
}
.ai-typing-indicator {
display: flex;
gap: 4px;
padding: 12px 14px;
background: var(--sentient-bg-tertiary);
border-radius: var(--sentient-radius-md);
width: fit-content;
}
.ai-typing-indicator span {
width: 8px;
height: 8px;
background: var(--sentient-text-muted);
border-radius: 50%;
animation: typing 1.4s infinite;
}
.ai-typing-indicator span:nth-child(2) { animation-delay: 0.2s; }
.ai-typing-indicator span:nth-child(3) { animation-delay: 0.4s; }
@keyframes typing {
0%, 60%, 100% { transform: translateY(0); opacity: 0.4; }
30% { transform: translateY(-4px); opacity: 1; }
}
.ai-quick-actions {
padding: 12px 16px;
border-top: 1px solid var(--sentient-border);
}
.quick-actions-label {
display: block;
font-size: 10px;
font-weight: 600;
color: var(--sentient-text-muted);
text-transform: uppercase;
letter-spacing: 0.5px;
margin-bottom: 10px;
}
.quick-actions-grid {
display: flex;
flex-wrap: wrap;
gap: 6px;
}
.quick-action-btn {
padding: 6px 10px;
background: var(--sentient-bg-tertiary);
border: 1px solid var(--sentient-border);
border-radius: var(--sentient-radius-sm);
color: var(--sentient-text-secondary);
font-family: var(--sentient-font-family);
font-size: 11px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s ease;
}
.quick-action-btn:hover {
background: var(--sentient-bg-hover);
border-color: var(--sentient-accent);
color: var(--sentient-accent);
}
.ai-panel-input {
display: flex;
gap: 8px;
padding: 12px 16px;
border-top: 1px solid var(--sentient-border);
}
.ai-input {
flex: 1;
padding: 10px 14px;
background: var(--sentient-bg-tertiary);
border: 1px solid var(--sentient-border);
border-radius: var(--sentient-radius-md);
color: var(--sentient-text-primary);
font-family: var(--sentient-font-family);
font-size: 13px;
}
.ai-input::placeholder { color: var(--sentient-text-muted); }
.ai-input:focus { outline: none; border-color: var(--sentient-accent); }
.ai-send-btn {
width: 40px;
height: 40px;
display: flex;
align-items: center;
justify-content: center;
background: var(--sentient-accent);
border: none;
border-radius: var(--sentient-radius-md);
color: #000;
font-size: 16px;
cursor: pointer;
}
.ai-send-btn:hover { background: #d4ff4a; }
/* =============================================================================
STAT CARDS
============================================================================= */
.stat-cards {
display: flex;
gap: 12px;
margin-bottom: 20px;
}
.stat-card {
flex: 1;
display: flex;
align-items: center;
gap: 12px;
padding: 16px;
background: var(--sentient-bg-card);
border: 1px solid var(--sentient-border);
border-radius: var(--sentient-radius-md);
}
.stat-card-icon {
width: 40px;
height: 40px;
display: flex;
align-items: center;
justify-content: center;
background: var(--sentient-bg-tertiary);
border-radius: var(--sentient-radius-sm);
font-size: 18px;
}
.stat-card-content { flex: 1; }
.stat-card-label {
font-size: 11px;
color: var(--sentient-text-muted);
text-transform: uppercase;
letter-spacing: 0.3px;
margin-bottom: 4px;
}
.stat-card-value {
font-size: 20px;
font-weight: 700;
}
.stat-card.highlight {
border-color: var(--sentient-accent);
}
.stat-card.highlight .stat-card-value {
color: var(--sentient-accent);
}
/* =============================================================================
APP HEADER
============================================================================= */
.app-header {
display: flex;
align-items: flex-start;
justify-content: space-between;
margin-bottom: 20px;
}
.app-title-section h1 {
font-size: 22px;
font-weight: 700;
margin: 0 0 4px 0;
}
.app-title-section p {
font-size: 13px;
color: var(--sentient-text-muted);
margin: 0;
}
.app-actions {
display: flex;
gap: 10px;
}
.app-btn-primary {
display: flex;
align-items: center;
gap: 6px;
padding: 10px 16px;
background: var(--sentient-accent);
border: none;
border-radius: var(--sentient-radius-sm);
color: #000;
font-family: var(--sentient-font-family);
font-size: 13px;
font-weight: 600;
cursor: pointer;
}
.app-btn-primary:hover { background: #d4ff4a; }
.app-btn-secondary {
width: 36px;
height: 36px;
display: flex;
align-items: center;
justify-content: center;
background: var(--sentient-bg-tertiary);
border: 1px solid var(--sentient-border);
border-radius: var(--sentient-radius-sm);
color: var(--sentient-text-secondary);
font-size: 16px;
cursor: pointer;
}
.app-btn-secondary:hover {
background: var(--sentient-bg-hover);
color: var(--sentient-text-primary);
}
/* =============================================================================
DATA TABLE
============================================================================= */
.data-table-container {
flex: 1;
background: var(--sentient-bg-card);
border: 1px solid var(--sentient-border);
border-radius: var(--sentient-radius-lg);
overflow: hidden;
}
.data-table {
width: 100%;
border-collapse: collapse;
}
.data-table thead {
background: var(--sentient-bg-tertiary);
}
.data-table th {
padding: 12px 16px;
text-align: left;
font-size: 11px;
font-weight: 600;
color: var(--sentient-text-muted);
text-transform: uppercase;
letter-spacing: 0.5px;
border-bottom: 1px solid var(--sentient-border);
}
.data-table td {
padding: 14px 16px;
font-size: 13px;
color: var(--sentient-text-primary);
border-bottom: 1px solid var(--sentient-border);
}
.data-table tbody tr:hover {
background: var(--sentient-bg-tertiary);
}
.data-table tbody tr:last-child td {
border-bottom: none;
}
.status-badge {
display: inline-block;
padding: 4px 10px;
border-radius: 12px;
font-size: 11px;
font-weight: 600;
}
.status-badge.active {
background: rgba(34, 197, 94, 0.15);
color: var(--sentient-success);
}
.status-badge.pending {
background: rgba(245, 158, 11, 0.15);
color: var(--sentient-warning);
}
.status-badge.inactive {
background: rgba(239, 68, 68, 0.15);
color: var(--sentient-error);
}
.table-actions {
display: flex;
gap: 6px;
}
.table-action-btn {
padding: 6px 12px;
background: var(--sentient-bg-tertiary);
border: 1px solid var(--sentient-border);
border-radius: var(--sentient-radius-sm);
color: var(--sentient-text-secondary);
font-family: var(--sentient-font-family);
font-size: 12px;
cursor: pointer;
transition: all 0.2s ease;
}
.table-action-btn:hover {
background: var(--sentient-bg-hover);
color: var(--sentient-text-primary);
}
.table-action-btn.delete {
background: rgba(239, 68, 68, 0.1);
border-color: rgba(239, 68, 68, 0.3);
color: var(--sentient-error);
}
.table-action-btn.delete:hover {
background: rgba(239, 68, 68, 0.2);
}
.data-table-footer {
display: flex;
align-items: center;
justify-content: space-between;
padding: 12px 16px;
background: var(--sentient-bg-tertiary);
border-top: 1px solid var(--sentient-border);
}
.pagination-info {
font-size: 12px;
color: var(--sentient-text-muted);
}
.pagination-controls {
display: flex;
align-items: center;
gap: 4px;
}
.pagination-btn {
padding: 6px 12px;
background: var(--sentient-bg-card);
border: 1px solid var(--sentient-border);
border-radius: var(--sentient-radius-sm);
color: var(--sentient-text-secondary);
font-family: var(--sentient-font-family);
font-size: 12px;
cursor: pointer;
}
.pagination-btn:hover {
background: var(--sentient-bg-hover);
color: var(--sentient-text-primary);
}
.pagination-btn.active {
background: var(--sentient-accent);
border-color: var(--sentient-accent);
color: #000;
}
/* =============================================================================
SCROLLBAR
============================================================================= */
::-webkit-scrollbar { width: 6px; height: 6px; }
::-webkit-scrollbar-track { background: var(--sentient-bg-secondary); }
::-webkit-scrollbar-thumb { background: var(--sentient-bg-tertiary); border-radius: 3px; }
::-webkit-scrollbar-thumb:hover { background: var(--sentient-border-hover); }
</style>
</head>
<body>
<div class="suite-app">
<!-- Top Header Bar -->
<header class="suite-topbar">
<div class="topbar-left">
<nav class="topbar-tabs">
<button class="topbar-tab active">Dashboard</button>
<button class="topbar-tab">Analytics</button>
</nav>
<div class="topbar-app-launcher">
<button class="app-icon" data-app="chat" title="Chat">💬</button>
<button class="app-icon active" data-app="files" title="Files">📁</button>
<button class="app-icon" data-app="terminal" title="Terminal">⌨️</button>
<button class="app-icon" data-app="tasks" title="Tasks"></button>
<button class="app-icon" data-app="calendar" title="Calendar">📅</button>
<button class="app-icon" data-app="docs" title="Docs">📄</button>
<button class="app-icon" data-app="settings" title="Settings">⚙️</button>
</div>
</div>
<div class="topbar-right">
<button class="topbar-btn-primary">✨ New Intent</button>
<button class="topbar-btn-icon" title="Settings">⚙️</button>
</div>
</header>
<!-- Main Content Area -->
<main class="suite-main">
<!-- Left: Content Panel -->
<section class="suite-content-panel">
<!-- Stat Cards -->
<div class="stat-cards">
<div class="stat-card highlight">
<div class="stat-card-icon">📊</div>
<div class="stat-card-content">
<div class="stat-card-label">Total Records</div>
<div class="stat-card-value">12,847</div>
</div>
</div>
<div class="stat-card">
<div class="stat-card-icon"></div>
<div class="stat-card-content">
<div class="stat-card-label">Active</div>
<div class="stat-card-value">8,234</div>
</div>
</div>
<div class="stat-card">
<div class="stat-card-icon"></div>
<div class="stat-card-content">
<div class="stat-card-label">Pending</div>
<div class="stat-card-value">2,156</div>
</div>
</div>
<div class="stat-card">
<div class="stat-card-icon">📈</div>
<div class="stat-card-content">
<div class="stat-card-label">Growth</div>
<div class="stat-card-value">+24%</div>
</div>
</div>
</div>
<!-- App Header -->
<div class="app-header">
<div class="app-title-section">
<h1>Files Manager</h1>
<p>Manage your documents and media files</p>
</div>
<div class="app-actions">
<button class="app-btn-primary">+ Upload File</button>
<button class="app-btn-secondary">🔍</button>
<button class="app-btn-secondary"></button>
</div>
</div>
<!-- Data Table -->
<div class="data-table-container">
<table class="data-table">
<thead>
<tr>
<th>Name</th>
<th>Type</th>
<th>Size</th>
<th>Modified</th>
<th>Status</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
<tr>
<td>project-report.pdf</td>
<td>PDF Document</td>
<td>2.4 MB</td>
<td>Dec 13, 2025</td>
<td><span class="status-badge active">Active</span></td>
<td>
<div class="table-actions">
<button class="table-action-btn">View</button>
<button class="table-action-btn">Edit</button>
<button class="table-action-btn delete">Delete</button>
</div>
</td>
</tr>
<tr>
<td>dashboard-mockup.fig</td>
<td>Figma File</td>
<td>8.1 MB</td>
<td>Dec 12, 2025</td>
<td><span class="status-badge pending">Pending</span></td>
<td>
<div class="table-actions">
<button class="table-action-btn">View</button>
<button class="table-action-btn">Edit</button>
<button class="table-action-btn delete">Delete</button>
</div>
</td>
</tr>
<tr>
<td>api-documentation.md</td>
<td>Markdown</td>
<td>156 KB</td>
<td>Dec 11, 2025</td>
<td><span class="status-badge active">Active</span></td>
<td>
<div class="table-actions">
<button class="table-action-btn">View</button>
<button class="table-action-btn">Edit</button>
<button class="table-action-btn delete">Delete</button>
</div>
</td>
</tr>
<tr>
<td>backup-2025-12.zip</td>
<td>Archive</td>
<td>45.2 MB</td>
<td>Dec 10, 2025</td>
<td><span class="status-badge inactive">Archived</span></td>
<td>
<div class="table-actions">
<button class="table-action-btn">View</button>
<button class="table-action-btn">Edit</button>
<button class="table-action-btn delete">Delete</button>
</div>
</td>
</tr>
<tr>
<td>user-analytics.csv</td>
<td>Spreadsheet</td>
<td>890 KB</td>
<td>Dec 9, 2025</td>
<td><span class="status-badge active">Active</span></td>
<td>
<div class="table-actions">
<button class="table-action-btn">View</button>
<button class="table-action-btn">Edit</button>
<button class="table-action-btn delete">Delete</button>
</div>
</td>
</tr>
</tbody>
</table>
<div class="data-table-footer">
<span class="pagination-info">Showing 1-5 of 847 files</span>
<div class="pagination-controls">
<button class="pagination-btn"></button>
<button class="pagination-btn active">1</button>
<button class="pagination-btn">2</button>
<button class="pagination-btn">3</button>
<button class="pagination-btn">...</button>
<button class="pagination-btn">170</button>
<button class="pagination-btn"></button>
</div>
</div>
</div>
</section>
<!-- Right: AI Assistant Panel -->
<aside class="suite-ai-panel">
<div class="ai-panel-header">
<div class="ai-panel-title">
<span class="ai-avatar">🤖</span>
<div>
<h3>AI Developer</h3>
<p class="ai-status">Desenvolvendo: CRM Deloitte</p>
</div>
</div>
<button class="ai-panel-close"></button>
</div>
<div class="ai-panel-messages" id="ai-messages">
<div class="ai-message assistant">
<div class="ai-message-bubble">Olá! Sou o AI Developer. Como posso ajudar você hoje?</div>
</div>
<div class="ai-message assistant">
<div class="ai-message-bubble">Você pode me pedir para modificar campos, alterar cores, adicionar validações ou qualquer outra mudança no sistema.</div>
</div>
<div class="ai-message user">
<div class="ai-message-bubble">Adicione um campo de telefone no formulário de cadastro</div>
</div>
<div class="ai-message assistant">
<div class="ai-message-bubble">Perfeito! Adicionei o campo de telefone com máscara automática e validação de formato brasileiro.</div>
<span class="ai-message-action">Ver alterações</span>
</div>
</div>
<div class="ai-quick-actions">
<span class="quick-actions-label">AÇÕES RÁPIDAS</span>
<div class="quick-actions-grid">
<button class="quick-action-btn">Adicionar campo</button>
<button class="quick-action-btn">Mudar cor</button>
<button class="quick-action-btn">Adicionar validação</button>
<button class="quick-action-btn">Exportar dados</button>
</div>
</div>
<div class="ai-panel-input">
<input type="text" class="ai-input" placeholder="Digite suas modificações..." id="ai-input">
<button class="ai-send-btn"></button>
</div>
</aside>
</main>
</div>
<script>
// Tab switching
document.querySelectorAll('.topbar-tab').forEach(tab => {
tab.addEventListener('click', function() {
document.querySelectorAll('.topbar-tab').forEach(t => t.classList.remove('active'));
this.classList.add('active');
});
});
// App icon switching
document.querySelectorAll('.app-icon').forEach(icon => {
icon.addEventListener('click', function() {
document.querySelectorAll('.app-icon').forEach(i => i.classList.remove('active'));
this.classList.add('active');
});
});
// Quick actions
document.querySelectorAll('.quick-action-btn').forEach(btn => {
btn.addEventListener('click', function() {
const action = this.textContent;
addMessage('user', action);
setTimeout(() => {
addMessage('assistant', `Ação "${action}" executada com sucesso!`);
}, 1000);
});
});
// Send message
document.querySelector('.ai-send-btn').addEventListener('click', sendMessage);
document.getElementById('ai-input').addEventListener('keypress', function(e) {
if (e.key === 'Enter') sendMessage();
});
function sendMessage() {
const input = document.getElementById('ai-input');
const message = input.value.trim();
if (!message) return;
addMessage('user', message);
input.value = '';
setTimeout(() => {
addMessage('assistant', `Entendido! Processando: "${message}"`);
}, 1500);
}
function addMessage(type, content) {
const container = document.getElementById('ai-messages');
const div = document.createElement('div');
div.className = `ai-message ${type}`;
div.innerHTML = `<div class="ai-message-bubble">${content}</div>`;
container.appendChild(div);
container.scrollTop = container.scrollHeight;
}
</script>
</body>
</html>

View file

@ -1,98 +0,0 @@
<!-- =============================================================================
BOTUI SUITE - BASE LAYOUT
Sentient Theme with AI Assistant Panel
============================================================================= -->
<link rel="stylesheet" href="/themes/sentient/sentient.css">
<link rel="stylesheet" href="/suite/base-layout.css">
<div class="suite-app sentient-theme">
<!-- Top Header Bar -->
<header class="suite-topbar">
<!-- Left: Navigation Tabs -->
<div class="topbar-left">
<nav class="topbar-tabs">
<button class="topbar-tab active">Dashboard</button>
<button class="topbar-tab">Analytics</button>
</nav>
<!-- App Launcher -->
<div class="topbar-app-launcher">
<button class="app-icon" data-app="chat" title="Chat">
<span>💬</span>
</button>
<button class="app-icon" data-app="files" title="Files">
<span>📁</span>
</button>
<button class="app-icon" data-app="terminal" title="Terminal">
<span>⌨️</span>
</button>
<button class="app-icon" data-app="tasks" title="Tasks">
<span></span>
</button>
<button class="app-icon" data-app="calendar" title="Calendar">
<span>📅</span>
</button>
<button class="app-icon" data-app="docs" title="Docs">
<span>📄</span>
</button>
<button class="app-icon" data-app="settings" title="Settings">
<span>⚙️</span>
</button>
</div>
</div>
<!-- Right: Actions -->
<div class="topbar-right">
<button class="topbar-btn-primary">
<span></span> New Intent
</button>
<button class="topbar-btn-icon" title="Settings">⚙️</button>
</div>
</header>
<!-- Main Content Area -->
<main class="suite-main">
<!-- Left: Content Panel -->
<section class="suite-content-panel" id="suite-content">
<!-- App content goes here -->
</section>
<!-- Right: AI Assistant Panel -->
<aside class="suite-ai-panel" id="ai-panel">
<div class="ai-panel-header">
<div class="ai-panel-title">
<span class="ai-avatar">🤖</span>
<div>
<h3>AI Developer</h3>
<p class="ai-status">Desenvolvendo: CRM Deloitte</p>
</div>
</div>
<button class="ai-panel-close" onclick="toggleAIPanel()"></button>
</div>
<div class="ai-panel-messages" id="ai-messages">
<!-- Messages will be inserted here -->
</div>
<div class="ai-quick-actions">
<span class="quick-actions-label">AÇÕES RÁPIDAS</span>
<div class="quick-actions-grid">
<button class="quick-action-btn">Adicionar campo</button>
<button class="quick-action-btn">Mudar cor</button>
<button class="quick-action-btn">Adicionar validação</button>
<button class="quick-action-btn">Exportar dados</button>
</div>
</div>
<div class="ai-panel-input">
<input type="text" class="ai-input" placeholder="Digite suas modificações..." id="ai-input">
<button class="ai-send-btn" onclick="sendAIMessage()">
<span></span>
</button>
</div>
</aside>
</main>
</div>
<script src="/suite/base-layout.js"></script>

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,19 @@
<div class="h-full flex flex-col bg-white overflow-hidden text-gray-800">
<div class="h-10 bg-gray-100 border-b border-gray-200 flex items-center px-4 space-x-2">
<button class="text-gray-400 hover:text-gray-600"><i class="fa-solid fa-arrow-left"></i></button>
<button class="text-gray-400 hover:text-gray-600"><i class="fa-solid fa-arrow-right"></i></button>
<button class="text-gray-400 hover:text-gray-600"><i class="fa-solid fa-rotate-right"></i></button>
<div class="flex-1 bg-white border border-gray-300 rounded px-3 py-1 flex items-center shadow-inner">
<i class="fa-solid fa-lock text-green-600 text-xs mr-2"></i>
<input type="text" class="flex-1 outline-none text-sm" value="https://generalbots.com" readonly>
</div>
<button class="text-gray-400 hover:text-gray-600"><i class="fa-solid fa-bars"></i></button>
</div>
<div class="flex-1 flex items-center justify-center bg-[#fafdfa]">
<div class="text-center text-gray-400">
<i class="fa-regular fa-compass text-5xl mb-4 text-brand-300"></i>
<h2 class="text-xl font-medium text-gray-600">Browser</h2>
<p class="mt-2 text-sm">Internet access starting soon...</p>
</div>
</div>
</div>

View file

@ -67,7 +67,7 @@
var WS_BASE_URL =
window.location.protocol === "https:" ? "wss://" : "ws://";
var WS_URL = WS_BASE_URL + window.location.host + "/ws/chat";
var WS_URL = WS_BASE_URL + window.location.host + "/ws";
var MessageType = {
EXTERNAL: 0,
@ -871,11 +871,21 @@
currentUserId +
"&bot_name=" +
currentBotName;
console.log("Connecting WebSocket to:", url);
ws = new WebSocket(url);
// Add connection timeout to detect silent failures
var connectionTimeout = setTimeout(function() {
if (ws.readyState !== WebSocket.OPEN) {
console.error("WebSocket connection timeout");
ws.close();
}
}, 5000);
ws.onopen = function () {
console.log("WebSocket connected");
reconnectAttempts = 0;
clearTimeout(connectionTimeout);
console.log("WebSocket connected to:", url);
disconnectNotified = false;
updateConnectionStatus("connected");
};
@ -886,7 +896,10 @@
console.log("Chat WebSocket received:", data);
// Ignore connection confirmation
if (data.type === "connected") return;
if (data.type === "connected") {
reconnectAttempts = 0;
return;
}
// Process system events (theme changes, etc)
if (data.event) {
@ -921,7 +934,9 @@
}
};
ws.onclose = function () {
ws.onclose = function (event) {
clearTimeout(connectionTimeout);
console.log("WebSocket closed:", event.code, event.reason);
updateConnectionStatus("disconnected");
if (!disconnectNotified) {
notify("Disconnected from chat server", "error");
@ -931,6 +946,9 @@
reconnectAttempts++;
updateConnectionStatus("connecting");
setTimeout(connectWebSocket, 1000 * reconnectAttempts);
} else {
console.error("Max reconnection attempts reached. Stopping reconnection.");
notify("Could not reconnect to chat server after multiple attempts", "error");
}
};
@ -1098,8 +1116,18 @@
})
.catch(function (e) {
console.error("Auth failed:", e);
notify("Failed to connect to chat server", "error");
setTimeout(proceedWithChatInit, 3000);
// Proceed with anonymous connection - WebSocket handler supports it
currentUserId = crypto.randomUUID ? crypto.randomUUID() : Date.now().toString();
currentSessionId = crypto.randomUUID ? crypto.randomUUID() : Date.now().toString();
currentBotId = botName;
currentBotName = botName;
console.log("Anonymous chat:", {
currentUserId: currentUserId,
currentSessionId: currentSessionId,
currentBotId: currentBotId,
currentBotName: currentBotName,
});
connectWebSocket();
});
}

View file

@ -140,7 +140,7 @@
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.9);
background: rgba(0, 0, 0, 0.8);
z-index: 10000;
display: flex;
align-items: center;
@ -168,7 +168,7 @@
max-width: 1400px;
height: 90%;
max-height: 900px;
background: #1a1a1a;
background: var(--bg, #1a1a1a);
border-radius: 12px;
display: flex;
flex-direction: column;
@ -190,8 +190,8 @@
justify-content: space-between;
align-items: center;
padding: 12px 20px;
background: #252525;
border-bottom: 1px solid #333;
background: var(--surface, #252525);
border-bottom: 1px solid var(--border, #333);
}
.projector-title-section {
@ -205,7 +205,7 @@
}
.projector-title {
color: #fff;
color: var(--text, #fff);
font-size: 16px;
font-weight: 500;
max-width: 400px;
@ -222,7 +222,7 @@
.projector-btn {
background: transparent;
border: none;
color: #aaa;
color: var(--text-muted, #aaa);
font-size: 18px;
padding: 8px;
cursor: pointer;
@ -231,13 +231,13 @@
}
.projector-btn:hover {
background: #333;
color: #fff;
background: var(--surface-hover, #333);
color: var(--text, #fff);
}
.projector-btn.close-btn:hover {
background: #e74c3c;
color: #fff;
background: var(--error, #e74c3c);
color: var(--text, #fff);
}
/* Content Area */
@ -248,7 +248,7 @@
justify-content: center;
overflow: hidden;
position: relative;
background: #000;
background: var(--bg, #000);
}
/* Loading */
@ -257,7 +257,7 @@
flex-direction: column;
align-items: center;
gap: 16px;
color: #888;
color: var(--text-muted, #888);
}
.projector-loading.hidden {
@ -267,8 +267,8 @@
.loading-spinner {
width: 40px;
height: 40px;
border: 3px solid #333;
border-top-color: #667eea;
border: 3px solid var(--surface-hover, #333);
border-top-color: var(--accent, #667eea);
border-radius: 50%;
animation: spin 1s linear infinite;
}
@ -321,7 +321,7 @@
width: 100%;
height: 100%;
overflow: auto;
background: #1e1e1e;
background: var(--bg, #1e1e1e);
padding: 20px;
}
@ -330,7 +330,7 @@
font-family: 'Fira Code', 'Monaco', 'Consolas', monospace;
font-size: 14px;
line-height: 1.5;
color: #d4d4d4;
color: var(--text, #d4d4d4);
}
.projector-code.line-numbers pre {
@ -343,7 +343,7 @@
display: inline-block;
width: 40px;
padding-right: 20px;
color: #666;
color: var(--text-muted, #666);
text-align: right;
}
@ -357,7 +357,7 @@
}
.slide-container {
background: #fff;
background: var(--bg, #fff);
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
aspect-ratio: 16/9;
max-width: 90%;
@ -379,7 +379,7 @@
width: 100%;
height: 100%;
border: none;
background: #fff;
background: var(--bg, #fff);
}
/* Markdown Viewer */
@ -388,20 +388,20 @@
height: 100%;
overflow: auto;
padding: 40px;
background: #fff;
color: #333;
background: var(--bg, #fff);
color: var(--text, #333);
}
.projector-markdown h1,
.projector-markdown h2,
.projector-markdown h3 {
color: #1a1a1a;
color: var(--text, #1a1a1a);
margin-top: 24px;
margin-bottom: 12px;
}
.projector-markdown code {
background: #f0f0f0;
background: var(--surface, #f0f0f0);
padding: 2px 6px;
border-radius: 4px;
font-family: monospace;
@ -416,8 +416,8 @@
/* Controls */
.projector-controls {
padding: 12px 20px;
background: #252525;
border-top: 1px solid #333;
background: var(--surface, #252525);
border-top: 1px solid var(--border, #333);
}
.media-controls,
@ -438,9 +438,9 @@
}
.control-btn {
background: #333;
background: var(--surface-hover, #333);
border: none;
color: #fff;
color: var(--text, #fff);
padding: 8px 12px;
border-radius: 6px;
cursor: pointer;
@ -449,11 +449,11 @@
}
.control-btn:hover {
background: #444;
background: var(--surface-active, #444);
}
.control-btn.active {
background: #667eea;
background: var(--accent, #667eea);
}
.play-btn {
@ -475,7 +475,7 @@
height: 6px;
-webkit-appearance: none;
appearance: none;
background: #444;
background: var(--surface-active, #444);
border-radius: 3px;
cursor: pointer;
}
@ -484,7 +484,7 @@
-webkit-appearance: none;
width: 14px;
height: 14px;
background: #667eea;
background: var(--accent, #667eea);
border-radius: 50%;
cursor: pointer;
}
@ -492,14 +492,14 @@
.progress-bar::-moz-range-thumb {
width: 14px;
height: 14px;
background: #667eea;
background: var(--accent, #667eea);
border-radius: 50%;
cursor: pointer;
border: none;
}
.time-display {
color: #888;
color: var(--text-muted, #888);
font-size: 12px;
min-width: 100px;
text-align: right;
@ -511,7 +511,7 @@
height: 4px;
-webkit-appearance: none;
appearance: none;
background: #444;
background: var(--surface-active, #444);
border-radius: 2px;
cursor: pointer;
}
@ -520,7 +520,7 @@
-webkit-appearance: none;
width: 12px;
height: 12px;
background: #fff;
background: var(--bg, #fff);
border-radius: 50%;
cursor: pointer;
}
@ -528,8 +528,8 @@
/* Speed/Theme Select */
.speed-select,
.theme-select {
background: #333;
color: #fff;
background: var(--surface-hover, #333);
color: var(--text, #fff);
border: none;
padding: 6px 10px;
border-radius: 6px;
@ -541,15 +541,15 @@
.image-info,
.code-info,
.zoom-level {
color: #888;
color: var(--text-muted, #888);
font-size: 14px;
}
.slide-nav input {
width: 50px;
background: #333;
background: var(--surface-hover, #333);
border: none;
color: #fff;
color: var(--text, #fff);
padding: 6px;
border-radius: 4px;
text-align: center;
@ -561,7 +561,7 @@
flex-direction: column;
align-items: center;
gap: 16px;
color: #e74c3c;
color: var(--error, #e74c3c);
}
.projector-error-icon {
@ -570,7 +570,7 @@
.projector-error-message {
font-size: 16px;
color: #888;
color: var(--text-muted, #888);
}
/* Responsive */
@ -958,12 +958,12 @@
const wrapper = document.createElement('div');
wrapper.style.textAlign = 'center';
wrapper.style.padding = '40px';
wrapper.style.color = '#888';
wrapper.style.color = 'var(--text-muted, #888)';
wrapper.innerHTML = `
<div style="font-size: 64px; margin-bottom: 20px;">📁</div>
<div style="font-size: 18px; margin-bottom: 10px;">Cannot preview this file type</div>
<a href="${data.source_url}" download style="color: #667eea; text-decoration: none;">
<a href="${data.source_url}" download style="color: var(--accent, #667eea); text-decoration: none;">
⬇️ Download File
</a>
`;

View file

@ -0,0 +1,604 @@
/* Chat Agent Mode — Z.ai toggle + OpenClaw multi-panel layout */
/* ============================================
AGENT / CHAT MODE TOGGLE (Z.ai style)
============================================ */
.chat-mode-toggle {
display: flex;
align-items: center;
gap: 2px;
background: var(--surface, #1a1a24);
border: 1px solid var(--border, #2a2a2a);
border-radius: 20px;
padding: 2px;
margin-right: 8px;
}
.chat-mode-btn {
padding: 5px 14px;
border: none;
border-radius: 18px;
font-family: 'Fira Code', monospace;
font-size: 12px;
font-weight: 600;
cursor: pointer;
transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1);
background: transparent;
color: var(--text-secondary, var(--text-muted));
letter-spacing: 0.3px;
}
.chat-mode-btn.active {
background: var(--accent);
color: var(--bg);
box-shadow: 0 2px 8px rgba(132, 214, 105, 0.3);
}
.chat-mode-btn:not(.active):hover {
color: var(--text, var(--bg));
background: var(--hover, rgba(255, 255, 255, 0.05));
}
/* ============================================
QUICK ACTION CHIPS (Z.ai style)
============================================ */
.quick-actions-container {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-bottom: 12px;
justify-content: center;
animation: quickActionsIn 0.4s ease;
}
@keyframes quickActionsIn {
from { opacity: 0; transform: translateY(8px); }
to { opacity: 1; transform: translateY(0); }
}
.quick-action-chip {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 8px 16px;
border: 1px solid var(--border, #2a2a2a);
border-radius: 24px;
background: var(--surface, #1a1a24);
color: var(--text, var(--bg));
font-family: 'Fira Code', monospace;
font-size: 12px;
font-weight: 500;
cursor: pointer;
transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1);
white-space: nowrap;
}
.quick-action-chip:hover {
border-color: var(--accent);
background: rgba(132, 214, 105, 0.08);
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(132, 214, 105, 0.15);
}
.quick-action-chip:active {
transform: translateY(0);
}
.quick-action-icon {
font-size: 14px;
}
/* ============================================
AGENT MODE MULTI-PANEL LAYOUT
============================================ */
.chat-layout.agent-mode {
max-width: none;
padding: 0;
display: grid;
grid-template-columns: 48px 1fr 1fr;
grid-template-rows: 1fr auto auto;
gap: 0;
}
/* Agent Mode Left Sidebar */
.agent-sidebar {
display: none;
grid-row: 1 / -1;
grid-column: 1;
background: var(--surface);
border-right: 1px solid var(--border);
flex-direction: column;
z-index: 50;
}
.agent-mode .agent-sidebar {
display: flex;
}
.agent-sidebar-item {
width: 48px;
height: 44px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: all 0.15s ease;
position: relative;
border-bottom: 1px solid var(--border);
background: transparent;
border-left: none;
border-right: none;
border-top: none;
color: var(--text-muted);
}
.agent-sidebar-item:hover {
background: var(--bg);
color: var(--text);
}
.agent-sidebar-item.active {
background: var(--bg);
border-left: 3px solid var(--accent);
color: var(--text);
}
.agent-sidebar-badge {
position: absolute;
top: 4px;
right: 4px;
min-width: 16px;
height: 16px;
padding: 0 4px;
border-radius: 8px;
background: var(--accent);
color: var(--bg);
font-size: 9px;
font-weight: 700;
display: flex;
align-items: center;
justify-content: center;
font-family: 'Fira Code', monospace;
}
/* Chat Panel (in agent mode) */
.agent-mode #messages {
padding: 16px;
}
.agent-mode footer {
grid-column: 2 / -1;
padding: 8px 16px;
border-top: 1px solid var(--border);
}
/* ============================================
THOUGHT PROCESS BLOCK
============================================ */
.thought-process {
margin: 12px 0;
border: 1px solid var(--border, var(--border));
border-radius: 8px;
overflow: hidden;
background: var(--surface, #f8f9fa);
}
.thought-process-header {
display: flex;
align-items: center;
gap: 8px;
padding: 10px 14px;
cursor: pointer;
font-family: 'Fira Code', monospace;
font-size: 12px;
font-weight: 600;
color: var(--text, var(--text));
background: var(--surface, var(--border));
transition: background 0.15s;
border: none;
width: 100%;
text-align: left;
}
.thought-process-header:hover {
background: var(--hover, #e8e9ea);
}
.thought-process-toggle {
transition: transform 0.2s;
font-size: 10px;
}
.thought-process.expanded .thought-process-toggle {
transform: rotate(90deg);
}
.thought-process-body {
display: none;
padding: 12px 14px;
font-size: 13px;
line-height: 1.6;
color: var(--text-secondary, var(--text-muted));
border-top: 1px solid var(--border, var(--border));
}
.thought-process.expanded .thought-process-body {
display: block;
}
/* ============================================
BROWSER PANEL
============================================ */
.agent-browser-panel {
display: none;
grid-column: 3;
grid-row: 1;
border-left: 1px solid var(--border);
flex-direction: column;
background: var(--bg);
}
.agent-mode .agent-browser-panel {
display: flex;
}
.browser-panel-header {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 12px;
background: var(--surface);
border-bottom: 1px solid var(--border);
font-family: 'Fira Code', monospace;
font-size: 12px;
font-weight: 600;
color: var(--text);
}
.browser-url-bar {
flex: 1;
padding: 4px 10px;
background: var(--bg);
border: 1px solid var(--border);
border-radius: 4px;
font-family: 'Fira Code', monospace;
font-size: 11px;
color: var(--text-muted);
}
.browser-panel-content {
flex: 1;
background: var(--bg);
}
.browser-panel-content iframe {
width: 100%;
height: 100%;
border: none;
}
.browser-panel-empty {
display: flex;
align-items: center;
justify-content: center;
height: 100%;
color: var(--text-muted);
font-family: 'Fira Code', monospace;
font-size: 13px;
}
/* ============================================
TERMINAL PANEL
============================================ */
.agent-terminal-panel {
display: none;
grid-column: 2;
grid-row: 2;
border-top: 1px solid var(--border);
flex-direction: column;
max-height: 200px;
min-height: 120px;
}
.agent-mode .agent-terminal-panel {
display: flex;
}
.terminal-panel-header {
display: flex;
align-items: center;
gap: 8px;
padding: 6px 12px;
background: #1e1e1e;
border-bottom: 1px solid #333;
font-family: 'Fira Code', monospace;
font-size: 11px;
font-weight: 600;
color: var(--accent);
}
.terminal-panel-content {
flex: 1;
overflow-y: auto;
background: #1e1e1e;
padding: 8px 12px;
font-family: 'Fira Code', monospace;
font-size: 12px;
line-height: 1.5;
color: #d4d4d4;
}
.terminal-line {
white-space: pre-wrap;
word-break: break-all;
}
.terminal-line.stderr {
color: #f87171;
}
.terminal-line.stdout {
color: #d4d4d4;
}
/* ============================================
AGENT INFO CARD
============================================ */
.agent-info-card {
display: none;
grid-column: 3;
grid-row: 2;
border-top: 1px solid var(--border);
border-left: 1px solid var(--border);
padding: 12px 16px;
background: var(--surface);
flex-direction: column;
gap: 8px;
}
.agent-mode .agent-info-card {
display: flex;
}
.agent-info-name {
display: flex;
align-items: center;
gap: 8px;
font-family: 'Fira Code', monospace;
font-size: 13px;
font-weight: 600;
color: var(--text);
}
.agent-info-dot {
width: 8px;
height: 8px;
border-radius: 50%;
background: var(--accent);
}
.agent-level-badge {
display: inline-block;
padding: 2px 8px;
border-radius: 4px;
font-family: 'Fira Code', monospace;
font-size: 10px;
font-weight: 700;
letter-spacing: 0.5px;
text-transform: uppercase;
}
.badge-evolved {
background: var(--accent);
color: var(--bg);
}
.badge-bred {
background: #f59e0b;
color: var(--bg);
}
.badge-wild {
background: #ef4444;
color: var(--bg);
}
.agent-info-model {
font-family: 'Fira Code', monospace;
font-size: 11px;
color: var(--text-muted);
}
.agent-info-toggles {
display: flex;
gap: 8px;
align-items: center;
}
.agent-toggle {
display: flex;
align-items: center;
gap: 4px;
font-family: 'Fira Code', monospace;
font-size: 11px;
color: var(--text-muted);
}
.agent-toggle-switch {
width: 28px;
height: 16px;
border-radius: 8px;
background: #ccc;
position: relative;
cursor: pointer;
transition: background 0.2s;
border: none;
padding: 0;
}
.agent-toggle-switch.on {
background: var(--accent);
}
.agent-toggle-switch::after {
content: '';
position: absolute;
top: 2px;
left: 2px;
width: 12px;
height: 12px;
border-radius: 50%;
background: var(--bg);
transition: transform 0.2s;
}
.agent-toggle-switch.on::after {
transform: translateX(12px);
}
/* ============================================
STEP COUNTER BAR
============================================ */
.agent-step-bar {
display: none;
grid-column: 1 / -1;
grid-row: 3;
align-items: center;
justify-content: space-between;
padding: 8px 16px;
background: var(--surface);
border-top: 1px solid var(--border);
}
.agent-mode .agent-step-bar {
display: flex;
}
.step-counter {
display: flex;
align-items: center;
gap: 12px;
font-family: 'Fira Code', monospace;
font-size: 13px;
color: var(--text);
}
.step-nav-btn {
width: 28px;
height: 28px;
border-radius: 50%;
border: 1px solid var(--border);
background: var(--bg);
color: var(--text-muted);
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.15s;
font-size: 14px;
padding: 0;
}
.step-nav-btn:hover {
border-color: var(--accent);
color: var(--accent);
}
.step-action-btns {
display: flex;
gap: 6px;
}
.step-action-btn {
width: 36px;
height: 36px;
border-radius: 50%;
border: none;
background: var(--accent);
color: var(--bg);
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.15s;
font-size: 16px;
padding: 0;
}
.step-action-btn:hover {
transform: scale(1.1);
box-shadow: 0 2px 8px rgba(132, 214, 105, 0.3);
}
/* ============================================
TODO LIST (OpenClaw style)
============================================ */
.agent-todo-list {
margin: 12px 0;
border: 1px solid var(--border, var(--border));
border-radius: 8px;
overflow: hidden;
}
.agent-todo-header {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 14px;
background: var(--surface, var(--border));
font-family: 'Fira Code', monospace;
font-size: 12px;
font-weight: 600;
color: var(--text, var(--text));
}
.agent-todo-count {
background: var(--accent);
color: var(--bg);
padding: 1px 6px;
border-radius: 10px;
font-size: 10px;
}
.agent-todo-item {
display: flex;
align-items: center;
gap: 10px;
padding: 8px 14px;
border-top: 1px solid var(--border, var(--border));
font-size: 13px;
color: var(--text, var(--text));
transition: opacity 0.2s;
}
.agent-todo-item.done {
opacity: 0.5;
text-decoration: line-through;
}
.agent-todo-check {
width: 16px;
height: 16px;
border-radius: 50%;
border: 2px solid #ccc;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.agent-todo-item.done .agent-todo-check {
background: var(--accent);
border-color: var(--accent);
color: var(--bg);
font-size: 10px;
}

180
ui/suite/css/desktop.css Normal file
View file

@ -0,0 +1,180 @@
.app-icon {
background: var(--surface-hover, #ffffff);
border: 1px solid var(--border, #e5e7eb);
box-shadow: 0px 4px 6px rgba(0, 0, 0, 0.05);
/* removed border */
/* removed border bottom */
}
.workspace-bg {
background-color: var(--bg, #fafdfa);
}
.workspace-grid {
background-image: linear-gradient(to right, var(--border, #f0fdf4) 1px, transparent 1px), linear-gradient(to bottom, var(--border, #f0fdf4) 1px, transparent 1px);
background-size: 40px 40px;
}
/* Custom scrollbar for terminal */
::-webkit-scrollbar {
width: 8px;
height: 8px;
}
::-webkit-scrollbar-track {
background: transparent;
}
::-webkit-scrollbar-thumb {
background: var(--surface-active, #333);
border-radius: 4px;
}
::-webkit-scrollbar-thumb:hover {
background: var(--border-hover, #555);
}
/* Window Manager Core Styles (replacing missing Tailwind classes) */
.window-element {
position: absolute;
width: 700px;
height: 500px;
background-color: var(--surface, white);
border-radius: 8px;
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
display: flex;
flex-direction: column;
border: 1px solid var(--border, #e5e7eb);
overflow: hidden;
z-index: 100;
}
.window-header {
height: 40px;
background-color: var(--surface, rgba(255, 255, 255, 0.95));
backdrop-filter: blur(4px);
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 16px;
border-bottom: 1px solid var(--border, #e5e7eb);
user-select: none;
cursor: move;
}
.window-header .font-mono {
font-family: 'Fira Code', monospace;
font-size: 12px;
font-weight: 700;
color: var(--accent, #16a34a); /* brand-600 */
letter-spacing: 0.025em;
}
.window-header-controls {
display: flex;
gap: 12px;
color: var(--text-muted, #9ca3af);
}
.window-header button {
background: none;
border: none;
cursor: pointer;
color: inherit;
font-size: 14px;
}
.window-header .btn-minimize:hover,
.window-header .btn-maximize:hover {
color: var(--text, #4b5563);
}
.window-header .btn-close:hover {
color: var(--error, #ef4444);
}
.window-body {
position: relative;
flex: 1 1 0%;
overflow-y: auto;
background-color: var(--bg, #fafdfa);
padding: 16px;
}
.w-\[700px\] { width: 700px; }
.h-\[500px\] { height: 500px; }
.bg-white { background-color: #fff; }
.rounded-lg { border-radius: 0.5rem; }
.shadow-2xl { box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25); }
.flex { display: flex; }
.flex-col { flex-direction: column; }
.border { border-width: 1px; border-style: solid; }
.border-gray-200 { border-color: #e5e7eb; }
.overflow-hidden { overflow: hidden; }
.absolute { position: absolute; }
.h-10 { height: 2.5rem; }
.bg-white\/95 { background-color: var(--surface, rgba(255, 255, 255, 0.95)); }
.backdrop-blur { backdrop-filter: blur(8px); }
.items-center { align-items: center; }
.justify-between { justify-content: space-between; }
.px-4 { padding-left: 1rem; padding-right: 1rem; }
.border-b { border-bottom-width: 1px; }
.select-none { user-select: none; }
.cursor-move { cursor: move; }
.font-mono { font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; }
.text-xs { font-size: 0.75rem; line-height: 1rem; }
.font-bold { font-weight: 700; }
.text-brand-600 { color: var(--accent, #84d669); }
.tracking-wide { letter-spacing: 0.025em; }
.space-x-3 > :not([hidden]) ~ :not([hidden]) { --tw-space-x-reverse: 0; margin-right: calc(0.75rem * var(--tw-space-x-reverse)); margin-left: calc(0.75rem * calc(1 - var(--tw-space-x-reverse))); }
.text-gray-400 { color: var(--text-muted, #9ca3af); }
.hover\:text-gray-600:hover { color: var(--text, #4b5563); }
.hover\:text-red-500:hover { color: var(--error, #ef4444); }
.relative { position: relative; }
.flex-1 { flex: 1 1 0%; }
.overflow-y-auto { overflow-y: auto; }
.bg-\[\#fafdfa\] { background-color: var(--bg, #fafdfa); }
.window-header {
height: 40px;
background-color: var(--surface, rgba(255, 255, 255, 0.95));
border-bottom: 1px solid var(--border, #e5e7eb);
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 16px;
user-select: none;
cursor: move;
}
.window-header .font-mono {
font-family: 'Fira Code', monospace;
font-size: 12px;
font-weight: 700;
color: var(--accent, #16a34a);
letter-spacing: 0.025em;
}
.window-header .flex {
display: flex;
gap: 12px;
color: var(--text-muted, #9ca3af);
}
.window-header button {
background: transparent;
border: none;
cursor: pointer;
transition: color 0.15s ease;
}
.window-header button:hover {
color: var(--text, #4b5563);
}
.window-header .btn-close:hover {
color: var(--error, #ef4444);
}
.window-content {
flex: 1;
overflow-y: auto;
position: relative;
background-color: var(--bg, #ffffff);
}

9
ui/suite/css/vendor/all.min.css vendored Normal file

File diff suppressed because one or more lines are too long

Binary file not shown.

Binary file not shown.

Binary file not shown.

536
ui/suite/desktop.html Normal file
View file

@ -0,0 +1,536 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>BUILD V3 - Web Desktop Environment</title>
<!-- Link to the existing compiled CSS -->
<link rel="stylesheet" href="/suite/css/app.css" />
<link rel="stylesheet" href="/suite/css/base.css" />
<link rel="stylesheet" href="/suite/css/theme-sentient.css" />
<link rel="stylesheet" href="/suite/css/desktop.css" />
<!-- Local JS requirements per AGENTS.md / UI.md -->
<script src="/suite/js/vendor/htmx.min.js"></script>
<script src="/suite/js/window-manager.js"></script>
<script src="/suite/js/theme-manager.js"></script>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Fira Code', 'Fira Sans', Arial, sans-serif;
background: var(--bg, #ffffff);
color: var(--text, #333333);
overflow: hidden;
height: 100vh;
width: 100vw;
margin: 0;
padding: 0;
display: flex;
}
/* Core Layout replicating BUILD V3 screenshot styling */
.build-container {
width: 100%;
height: 100%;
display: flex;
position: absolute;
inset: 0;
z-index: 1000;
background: var(--bg, #ffffff);
}
/* Left Sidebar */
.sidebar {
width: 51px;
height: 100%;
background: var(--bg-secondary, #f8f8f8);
border-right: 1px solid var(--border-color, #f0f1f2);
display: flex;
flex-direction: column;
z-index: 100;
}
.sidebar-item {
width: 51px;
height: 50px;
border-bottom: 1px solid var(--border-color, #f0f1f2);
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: all 0.15s ease;
}
.sidebar-item:hover {
background: var(--bg, #ffffff);
}
.sidebar-item.active {
background: var(--bg, #ffffff);
border-left: 3px solid var(--primary, #84d669);
}
.sidebar-icon {
width: 30px;
height: 30px;
stroke: var(--text-secondary, #3b3b3b);
opacity: 0.6;
transition: stroke 0.15s ease, opacity 0.15s ease;
}
.sidebar-item:hover .sidebar-icon {
opacity: 1;
stroke: var(--text, #3b3b3b);
}
/* Main Content wrapper */
.main-wrapper {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
}
/* Top Navigation Tabs */
.tabs-container {
display: flex;
flex-direction: column;
background: var(--bg-secondary, #f8f8f8);
border-bottom: 1px solid var(--border-color, #f0f1f2);
z-index: 100;
}
.tabs-row {
display: flex;
height: 34px;
}
.main-tab {
height: 34px;
min-width: 169px;
flex: 1;
border-right: 1px solid var(--border-color, #f0f1f2);
display: flex;
align-items: center;
padding: 0 18px;
cursor: pointer;
transition: background 0.15s ease;
}
.main-tab:hover {
background: var(--bg, #ffffff);
}
.main-tab.active {
background: var(--primary, #84d669);
}
.main-tab.active .main-tab-content {
color: var(--bg, #ffffff);
}
.main-tab-content {
display: flex;
align-items: center;
gap: 4px;
font-family: 'Fira Code', monospace;
font-size: 14px;
font-weight: 500;
color: var(--text-secondary, #3b3b3b);
transition: color 0.15s ease;
}
/* Workspace (Where windows float) */
.workspace {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
background: var(--bg, #ffffff);
position: relative;
}
/* The Panel Grid (Desktop Icons) */
.panel-section {
flex: 1;
padding: 26px 33px;
overflow-y: auto;
z-index: 10;
position: absolute;
inset: 0;
pointer-events: none;
}
/* Interactive Desktop Icons triggering HTMX */
.desktop-icons-container {
display: flex;
flex-direction: column;
gap: 20px;
align-items: center;
width: 100px;
pointer-events: auto;
}
.desktop-icon {
display: flex;
flex-direction: column;
align-items: center;
width: 80px;
cursor: pointer;
position: relative;
gap: 8px;
}
.app-icon {
width: 64px;
height: 64px;
border-radius: 16px;
background: var(--surface-hover, #ffffff);
border: 1px solid var(--border, #e5e7eb);
box-shadow: 0px 4px 6px rgba(0, 0, 0, 0.05);
display: flex;
align-items: center;
justify-content: center;
transition: transform 0.15s ease;
}
.desktop-icon:hover .app-icon {
transform: scale(1.05);
border-color: var(--accent, #84d669);
box-shadow: 0px 6px 12px rgba(0, 0, 0, 0.1);
}
.app-icon svg {
width: 32px;
height: 32px;
stroke: var(--text, #374151);
transition: stroke 0.15s ease;
}
.desktop-icon:hover .app-icon svg {
stroke: var(--accent, #84d669);
}
.desktop-icon-label {
font-family: 'Fira Code', monospace;
font-size: 12px;
font-weight: 600;
color: var(--text-secondary, #374151);
background: transparent;
backdrop-filter: blur(4px);
padding: 2px 8px;
border-radius: 4px;
text-align: center;
}
.bg-grid { display: none; }
.bg-svg { display: none !important; }
/* Bottom Taskbar */
.toolbar {
height: 50px;
background: var(--surface);
border-top: 1px solid var(--border);
display: flex;
align-items: center;
padding: 0 8px;
z-index: 100;
position: relative;
}
#taskbar-apps {
display: flex;
flex: 1;
height: 100%;
align-items: center;
gap: 0px;
}
.toolbar-time {
font-family: 'Fira Code', monospace;
font-size: 14px;
color: var(--text-secondary, #3b3b3b);
text-align: right;
line-height: 1.4;
padding: 0 10px;
margin-left: auto;
}
/* Taskbar Items generated by WindowManager */
.taskbar-item {
width: 40px;
height: 40px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: all 0.15s ease;
border-bottom: 2px solid transparent;
}
.taskbar-item:hover {
background: var(--bg-hover, #f8f8f8);
}
.taskbar-item.active {
border-bottom-color: var(--primary, #84d669);
background: linear-gradient(to bottom, transparent 50%, var(--primary-alpha-10, rgba(132, 214, 105, 0.1)) 100%);
}
</style>
</head>
<body>
<!-- Minibar Component (Phase 8) -->
<div hx-get="/suite/partials/minibar.html" hx-trigger="load" hx-swap="outerHTML"></div>
<div class="build-container">
<!-- Left Sidebar -->
<aside class="sidebar">
<div class="sidebar-item active" title="Home">
<svg class="sidebar-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"></path>
<polyline points="9 22 9 12 15 12 15 22"></polyline>
</svg>
</div>
<div class="sidebar-item" title="Search">
<svg class="sidebar-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="11" cy="11" r="8"></circle>
<line x1="21" y1="21" x2="16.65" y2="16.65"></line>
</svg>
</div>
<div class="sidebar-item" title="Terminal">
<svg class="sidebar-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polyline points="4 17 10 11 4 5"></polyline>
<line x1="12" y1="19" x2="20" y2="19"></line>
</svg>
</div>
<div class="sidebar-item" title="User">
<svg class="sidebar-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"></path>
<circle cx="12" cy="7" r="4"></circle>
</svg>
</div>
<div class="sidebar-item" title="Apps">
<svg class="sidebar-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<rect x="3" y="3" width="7" height="7"></rect>
<rect x="14" y="3" width="7" height="7"></rect>
<rect x="14" y="14" width="7" height="7"></rect>
<rect x="3" y="14" width="7" height="7"></rect>
</svg>
</div>
<div class="sidebar-item" style="margin-top: auto;" title="Settings">
<svg class="sidebar-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="12" cy="12" r="3"></circle>
<path
d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z">
</path>
</svg>
</div>
</aside>
<!-- Main Wrapper -->
<div class="main-wrapper">
<!-- Top Navigation Tabs -->
<div class="tabs-container">
<div class="breadcrumb-row"
style="display: flex; height: 34px; border-bottom: 1px solid var(--border-color, #f0f1f2); align-items: center; padding: 0 18px; font-family: 'Fira Code', monospace; font-size: 14px; color: var(--text-muted, #6b7280); gap: 8px;">
<span style="color: var(--text-secondary, #374151);">// DASHBOARD</span>
<span style="color: var(--text-muted, #9ca3af);">></span>
<span style="color: var(--text, #374151); font-weight: 600;">// E-COMMERCE APP DEVELOPMENT</span>
</div>
<div class="tabs-row">
<div class="main-tab">
<div class="main-tab-content"><span>//</span><span>PLAN</span></div>
</div>
<div class="main-tab active">
<div class="main-tab-content"><span>//</span><span>BUILD</span></div>
</div>
<div class="main-tab">
<div class="main-tab-content"><span>//</span><span>REVIEW</span></div>
</div>
<div class="main-tab">
<div class="main-tab-content"><span>//</span><span>DEPLOY</span></div>
</div>
<div class="main-tab">
<div class="main-tab-content"><span>//</span><span>MONITOR</span></div>
</div>
</div>
</div>
<!-- Workspace container where WindowManager operates -->
<div class="workspace" id="desktop-content">
<!-- Background Pattern -->
<div class="panel-section">
<div class="desktop-icons-container">
<div class="desktop-icon" data-app-id="vibe" data-app-title="Vibe"
hx-get="/suite/partials/vibe.html" hx-swap="none">
<div class="app-icon">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
stroke-linecap="round" stroke-linejoin="round">
<path d="M12 2v20M17 5H9.5a3.5 3.5 0 0 0 0 7h5a3.5 3.5 0 0 1 0 7H6" />
</svg>
</div>
<span class="desktop-icon-label">Vibe</span>
</div>
<div class="desktop-icon" data-app-id="tasks" data-app-title="Tasks"
hx-get="/suite/tasks/task-window.html" hx-swap="none">
<div class="app-icon">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
stroke-linecap="round" stroke-linejoin="round">
<path d="M9 11l3 3L22 4" />
<path d="M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11" />
</svg>
</div>
<span class="desktop-icon-label">Tasks</span>
</div>
<div class="desktop-icon" data-app-id="chat" data-app-title="Chat"
hx-get="/suite/partials/chat.html" hx-swap="none">
<div class="app-icon">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
stroke-linecap="round" stroke-linejoin="round">
<path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z" />
</svg>
</div>
<span class="desktop-icon-label">Chat</span>
</div>
<div class="desktop-icon" data-app-id="terminal" data-app-title="Terminal"
hx-get="/suite/terminal/terminal.html" hx-swap="none">
<div class="app-icon">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
stroke-linecap="round" stroke-linejoin="round">
<polyline points="4 17 10 11 4 5" />
<line x1="12" y1="19" x2="20" y2="19" />
</svg>
</div>
<span class="desktop-icon-label">Terminal</span>
</div>
<div class="desktop-icon" data-app-id="drive" data-app-title="Explorer"
hx-get="/suite/drive/drive.html" hx-swap="none">
<div class="app-icon">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
stroke-linecap="round" stroke-linejoin="round">
<path
d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z" />
</svg>
</div>
<span class="desktop-icon-label">Explorer</span>
</div>
<div class="desktop-icon" data-app-id="editor" data-app-title="Editor"
hx-get="/suite/editor.html" hx-swap="none">
<div class="app-icon">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
stroke-linecap="round" stroke-linejoin="round">
<polyline points="16 18 22 12 16 6" />
<polyline points="8 6 2 12 8 18" />
</svg>
</div>
<span class="desktop-icon-label">Editor</span>
</div>
<div class="desktop-icon" data-app-id="browser" data-app-title="Browser"
hx-get="/suite/browser/browser.html" hx-swap="none">
<div class="app-icon">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
stroke-linecap="round" stroke-linejoin="round">
<circle cx="12" cy="12" r="10" />
<polygon points="16.24 7.76 14.12 14.12 7.76 16.24 9.88 9.88 16.24 7.76" />
</svg>
</div>
<span class="desktop-icon-label">Browser</span>
</div>
</div>
</div>
</div>
<!-- Bottom Taskbar -->
<footer class="toolbar" id="taskbar">
<div id="taskbar-apps">
<!-- Taskbar items populated automatically by window-manager.js -->
</div>
<div class="toolbar-time" style="display: flex; align-items: center; gap: 15px;">
<div id="themeSelectorContainer"></div>
<div style="text-align: right;">
<div id="clock-time">00:00</div>
<div id="clock-date">01/01/2026</div>
</div>
</div>
</footer>
</div>
</div>
<!-- HTMX Intercepts and WindowManager Init as described in UI.md Phase 3 -->
<script>
document.addEventListener('DOMContentLoaded', () => {
// Initialize WindowManager
if (typeof window.WindowManager !== 'undefined') {
window.wm = window.WindowManager;
} else {
console.error("WindowManager class not loaded from window-manager.js");
}
// Initialize ThemeManager
if (typeof window.ThemeManager !== 'undefined') {
window.ThemeManager.init();
}
});
// Listen to HTMX afterRequest event
document.body.addEventListener('htmx:afterRequest', function (evt) {
const target = evt.detail.elt;
// Check if the click came from a desktop icon
if (target.classList.contains('desktop-icon')) {
const appId = target.getAttribute('data-app-id');
const title = target.getAttribute('data-app-title');
const htmlContent = evt.detail.xhr.response;
// Tell WindowManager to open it
if (window.wm) {
window.wm.open(appId, title, htmlContent);
}
}
// Ensure Theme dropdown is re-injected if wiped
if (window.ThemeManager) {
const container = document.getElementById('themeSelectorContainer');
if (container && !container.hasChildNodes()) {
// Quick and dirty way to re-init
window.ThemeManager.init();
}
}
});
// Simple Clock implementation matching the screenshot bottom right corner
setInterval(() => {
const now = new Date();
document.getElementById('clock-time').textContent = now.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
document.getElementById('clock-date').textContent = now.toLocaleDateString();
}, 1000);
</script>
</body>
</html>

View file

@ -17,7 +17,7 @@
<nav class="nav-section">
<div class="nav-item active"
hx-get="/api/drive/files?path=/"
hx-target="#file-grid"
hx-target="closest .window-body #file-grid"
hx-swap="innerHTML"
onclick="setActiveNav(this)">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
@ -29,7 +29,7 @@
<div class="nav-item"
hx-get="/api/drive/files?filter=shared"
hx-target="#file-grid"
hx-target="closest .window-body #file-grid"
hx-swap="innerHTML"
onclick="setActiveNav(this)">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
@ -43,7 +43,7 @@
<div class="nav-item"
hx-get="/api/drive/files?filter=recent"
hx-target="#file-grid"
hx-target="closest .window-body #file-grid"
hx-swap="innerHTML"
onclick="setActiveNav(this)">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
@ -55,7 +55,7 @@
<div class="nav-item"
hx-get="/api/drive/files?filter=starred"
hx-target="#file-grid"
hx-target="closest .window-body #file-grid"
hx-swap="innerHTML"
onclick="setActiveNav(this)">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
@ -66,7 +66,7 @@
<div class="nav-item"
hx-get="/api/drive/files?filter=trash"
hx-target="#file-grid"
hx-target="closest .window-body #file-grid"
hx-swap="innerHTML"
onclick="setActiveNav(this)">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
@ -174,7 +174,7 @@
<select class="sort-dropdown" id="sort-dropdown"
hx-get="/api/drive/files"
hx-target="#file-grid"
hx-target="closest .window-body #file-grid"
hx-swap="innerHTML"
hx-include="[name='path']">
<option value="name">Name</option>
@ -264,7 +264,7 @@
hx-delete="/api/drive/files"
hx-include=".file-checkbox:checked"
hx-confirm="Move selected items to trash?"
hx-target="#file-grid"
hx-target="closest .window-body #file-grid"
hx-swap="innerHTML">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polyline points="3 6 5 6 21 6"></polyline>
@ -322,7 +322,7 @@
<div class="upload-zone" id="upload-zone"
hx-post="/api/drive/upload"
hx-encoding="multipart/form-data"
hx-target="#file-grid"
hx-target="closest .window-body #file-grid"
hx-swap="innerHTML">
<input type="file" id="file-input" name="files" multiple hidden>
<input type="hidden" name="path" id="upload-path" value="/">
@ -352,7 +352,7 @@
<dialog class="modal" id="folder-modal">
<form class="modal-content"
hx-post="/api/drive/folder"
hx-target="#file-grid"
hx-target="closest .window-body #file-grid"
hx-swap="innerHTML"
hx-on::after-request="document.getElementById('folder-modal').close()">
<div class="modal-header">
@ -418,7 +418,7 @@
<dialog class="modal" id="copy-modal">
<form class="modal-content"
hx-post="/files/copy"
hx-target="#file-grid"
hx-target="closest .window-body #file-grid"
hx-swap="innerHTML"
hx-on::after-request="document.getElementById('copy-modal').close()">
<div class="modal-header">
@ -453,7 +453,7 @@
<dialog class="modal" id="move-modal">
<form class="modal-content"
hx-post="/files/move"
hx-target="#file-grid"
hx-target="closest .window-body #file-grid"
hx-swap="innerHTML"
hx-on::after-request="document.getElementById('move-modal').close()">
<div class="modal-header">
@ -562,7 +562,7 @@
</h3>
<p>Combine multiple documents into one</p>
<form hx-post="/docs/merge"
hx-target="#docs-result"
hx-target="closest .window-body #docs-result"
hx-swap="innerHTML"
hx-encoding="multipart/form-data">
<input type="file" name="files" multiple accept=".pdf,.docx,.doc,.txt" class="form-group" style="margin-bottom: 8px;">
@ -584,7 +584,7 @@
</h3>
<p>Convert between document formats</p>
<form hx-post="/docs/convert"
hx-target="#docs-result"
hx-target="closest .window-body #docs-result"
hx-swap="innerHTML"
hx-encoding="multipart/form-data">
<input type="file" name="file" accept=".pdf,.docx,.doc,.txt,.md,.html" class="form-group" style="margin-bottom: 8px;">
@ -612,7 +612,7 @@
</h3>
<p>Populate template with data</p>
<form hx-post="/docs/fill"
hx-target="#docs-result"
hx-target="closest .window-body #docs-result"
hx-swap="innerHTML"
hx-encoding="multipart/form-data">
<input type="file" name="template" accept=".docx,.doc" class="form-group" style="margin-bottom: 8px;">
@ -633,7 +633,7 @@
</h3>
<p>Export document in specified format</p>
<form hx-post="/docs/export"
hx-target="#docs-result"
hx-target="closest .window-body #docs-result"
hx-swap="innerHTML">
<input type="text" name="path" placeholder="File path (e.g., /documents/report.docx)" class="form-group" style="width: 100%; margin-bottom: 8px;">
<select name="format" class="sort-dropdown" style="width: 100%; margin-bottom: 8px;">
@ -657,7 +657,7 @@
</h3>
<p>Import document from URL or upload</p>
<form hx-post="/docs/import"
hx-target="#docs-result"
hx-target="closest .window-body #docs-result"
hx-swap="innerHTML"
hx-encoding="multipart/form-data">
<input type="url" name="url" placeholder="Document URL (optional)" class="form-group" style="width: 100%; margin-bottom: 8px;">

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,302 @@
/**
* Chat Agent Mode handles toggling between Agent and Chat mode,
* multi-panel layout management, and WebSocket message routing
* for thought process, terminal output, browser preview, and step tracking.
*/
(function () {
"use strict";
var agentMode = false;
var currentStep = 0;
var totalSteps = 0;
var terminalLineCount = 0;
function initAgentMode() {
setupModeToggle();
setupToggleSwitches();
setupStepNavigation();
setupQuickActions();
setupSidebarItems();
}
function setupModeToggle() {
var agentBtn = document.getElementById("modeAgentBtn");
var chatBtn = document.getElementById("modeChatBtn");
if (!agentBtn || !chatBtn) return;
agentBtn.addEventListener("click", function () {
setMode("agent");
});
chatBtn.addEventListener("click", function () {
setMode("chat");
});
}
function setMode(mode) {
var chatApp = document.getElementById("chat-app");
var agentBtn = document.getElementById("modeAgentBtn");
var chatBtn = document.getElementById("modeChatBtn");
var quickActions = document.getElementById("quickActions");
if (!chatApp || !agentBtn || !chatBtn) return;
agentMode = mode === "agent";
agentBtn.classList.toggle("active", agentMode);
chatBtn.classList.toggle("active", !agentMode);
if (agentMode) {
chatApp.classList.add("agent-mode");
if (quickActions) quickActions.style.display = "none";
} else {
chatApp.classList.remove("agent-mode");
if (quickActions) quickActions.style.display = "";
}
}
function setupToggleSwitches() {
var planToggle = document.getElementById("togglePlan");
var yoloToggle = document.getElementById("toggleYolo");
if (planToggle) {
planToggle.addEventListener("click", function () {
this.classList.toggle("on");
emitModeChange();
});
}
if (yoloToggle) {
yoloToggle.addEventListener("click", function () {
this.classList.toggle("on");
emitModeChange();
});
}
}
function emitModeChange() {
var planOn = document.getElementById("togglePlan");
var yoloOn = document.getElementById("toggleYolo");
var mode = "plan";
if (yoloOn && yoloOn.classList.contains("on")) {
mode = "yolo";
}
if (window.ws && window.ws.readyState === WebSocket.OPEN) {
window.ws.send(JSON.stringify({
type: "toggle_mode",
mode: mode
}));
}
}
function setupStepNavigation() {
var prevBtn = document.getElementById("stepPrev");
var nextBtn = document.getElementById("stepNext");
if (prevBtn) {
prevBtn.addEventListener("click", function () {
if (currentStep > 1) {
currentStep--;
updateStepCounter();
}
});
}
if (nextBtn) {
nextBtn.addEventListener("click", function () {
if (currentStep < totalSteps) {
currentStep++;
updateStepCounter();
}
});
}
}
function updateStepCounter() {
var display = document.getElementById("stepCounterText");
if (display) {
display.textContent = currentStep + " / " + totalSteps;
}
}
function setupQuickActions() {
var chips = document.querySelectorAll(".quick-action-chip");
chips.forEach(function (chip) {
chip.addEventListener("click", function () {
var action = this.getAttribute("data-action");
var prompts = {
"full-stack": "Create a full-stack web application",
"writing": "Help me write ",
"data-insight": "Analyze data and provide insights",
"magic-design": "Design a beautiful UI for "
};
var input = document.getElementById("messageInput");
if (input && prompts[action]) {
input.value = prompts[action];
input.focus();
}
});
});
}
function setupSidebarItems() {
var items = document.querySelectorAll(".agent-sidebar-item");
items.forEach(function (item) {
item.addEventListener("click", function () {
items.forEach(function (i) { i.classList.remove("active"); });
this.classList.add("active");
});
});
}
/* ===========================================
Agent Mode WebSocket Message Handlers
=========================================== */
function handleAgentMessage(data) {
if (!agentMode) return;
switch (data.type) {
case "thought_process":
renderThoughtProcess(data.content);
break;
case "terminal_output":
appendTerminalLine(data.line, data.stream);
break;
case "browser_ready":
showBrowserPreview(data.url);
break;
case "step_progress":
currentStep = data.current;
totalSteps = data.total;
updateStepCounter();
break;
case "step_complete":
break;
case "todo_update":
renderTodoList(data.todos);
break;
case "agent_status":
updateAgentInfo(data);
break;
case "file_created":
incrementBadge("explorerBadge");
break;
}
}
function renderThoughtProcess(content) {
var messages = document.getElementById("messages");
if (!messages) return;
var block = document.createElement("div");
block.className = "thought-process";
block.innerHTML =
'<button class="thought-process-header">' +
'<span class="thought-process-toggle">▶</span>' +
'<span>Thought Process</span>' +
"</button>" +
'<div class="thought-process-body">' + escapeForHtml(content) + "</div>";
var header = block.querySelector(".thought-process-header");
header.addEventListener("click", function () {
block.classList.toggle("expanded");
});
messages.appendChild(block);
}
function appendTerminalLine(text, stream) {
var terminal = document.getElementById("terminalPanelContent");
if (!terminal) return;
var line = document.createElement("div");
line.className = "terminal-line " + (stream || "stdout");
line.textContent = text;
terminal.appendChild(line);
terminal.scrollTop = terminal.scrollHeight;
terminalLineCount++;
incrementBadge("terminalBadge");
}
function showBrowserPreview(url) {
var content = document.getElementById("browserPanelContent");
var urlBar = document.getElementById("browserUrlBar");
if (!content || !urlBar) return;
urlBar.value = url;
content.innerHTML = '<iframe src="' + url + '" sandbox="allow-scripts allow-same-origin"></iframe>';
}
function renderTodoList(todos) {
var messages = document.getElementById("messages");
if (!messages) return;
var existing = messages.querySelector(".agent-todo-list:last-child");
if (existing) existing.remove();
var list = document.createElement("div");
list.className = "agent-todo-list";
var headerHtml = '<div class="agent-todo-header">' +
'<span>📋 Todos</span>' +
'<span class="agent-todo-count">' + todos.length + "</span>" +
"</div>";
var itemsHtml = todos.map(function (todo) {
var doneClass = todo.done ? " done" : "";
var checkMark = todo.done ? "✓" : "";
return '<div class="agent-todo-item' + doneClass + '">' +
'<span class="agent-todo-check">' + checkMark + "</span>" +
'<span>' + escapeForHtml(todo.text) + "</span>" +
"</div>";
}).join("");
list.innerHTML = headerHtml + itemsHtml;
messages.appendChild(list);
}
function updateAgentInfo(data) {
var nameEl = document.getElementById("agentNameDisplay");
var levelEl = document.getElementById("agentLevelBadge");
var modelEl = document.getElementById("agentModelDisplay");
if (nameEl && data.name) nameEl.textContent = data.name;
if (levelEl && data.level) {
levelEl.textContent = data.level;
levelEl.className = "agent-level-badge badge-" + data.level.toLowerCase();
}
if (modelEl && data.model) {
modelEl.textContent = data.model + " — " + (data.usage || 0) + "%";
}
}
function incrementBadge(badgeId) {
var badge = document.getElementById(badgeId);
if (!badge) return;
var count = parseInt(badge.textContent, 10) || 0;
badge.textContent = count + 1;
badge.style.display = "";
}
function escapeForHtml(text) {
var div = document.createElement("div");
div.textContent = text || "";
return div.innerHTML;
}
/* ===========================================
Expose to global scope
=========================================== */
window.AgentMode = {
init: initAgentMode,
handleMessage: handleAgentMessage,
setMode: setMode,
isActive: function () { return agentMode; }
};
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", initAgentMode);
} else {
initAgentMode();
}
})();

View file

@ -192,7 +192,6 @@
document.body.addEventListener("htmx:wsOpen", () => {
updateConnectionStatus("connected");
reconnectAttempts = 0;
});
document.body.addEventListener("htmx:wsClose", () => {
@ -205,6 +204,10 @@
function handleWebSocketMessage(message) {
const messageType = message.type || message.event;
if (messageType === "connected") {
reconnectAttempts = 0;
}
// Debug logging
console.log("handleWebSocketMessage called with:", { messageType, message });

View file

@ -59,10 +59,19 @@ const Omnibox = {
this.chatInput = document.getElementById("omniboxChatInput");
this.modeToggle = document.getElementById("omniboxModeToggle");
this.bindEvents();
// Only bind events if all required elements exist
if (this.input && this.backdrop) {
this.bindEvents();
}
},
bindEvents() {
// Defensive: ensure elements exist before binding
if (!this.input || !this.backdrop) {
console.warn("[Omnibox] Required elements not found, skipping event binding");
return;
}
// Input focus/blur
this.input.addEventListener("focus", () => this.open());
this.backdrop.addEventListener("click", () => this.close());
@ -1032,9 +1041,11 @@ document.addEventListener("DOMContentLoaded", () => {
}
}
// Skip SPA initialization on auth pages (login, register, etc.)
// Skip SPA initialization on auth pages (login, register, etc.) and desktop
if (window.location.pathname.startsWith("/auth/")) {
console.log("[SPA] Skipping initialization on auth page");
} else if (document.getElementById('desktop-content')) {
console.log("[SPA] Skipping initialization on desktop page");
} else if (document.readyState === "complete") {
setTimeout(initialLoad, 50);
} else {

View file

@ -6,11 +6,11 @@ const ThemeManager = (() => {
// Bot ID to theme mapping (configured via config.csv theme-base field)
const botThemeMap = {
// Default bot uses light theme with brown accents
"default": "light",
default: "light",
// Cristo bot uses typewriter theme (classic typewriter style)
"cristo": "typewriter",
cristo: "typewriter",
// Salesianos bot uses light theme with blue accents
"salesianos": "light",
salesianos: "light",
};
// Detect current bot from URL path
@ -98,72 +98,170 @@ const ThemeManager = (() => {
setTimeout(() => {
// Get the theme's colors from CSS variables
const rootStyle = getComputedStyle(document.documentElement);
const primary = rootStyle.getPropertyValue("--primary")?.trim() || "#3b82f6";
const background = rootStyle.getPropertyValue("--background")?.trim() || "0 0% 100%";
const foreground = rootStyle.getPropertyValue("--foreground")?.trim() || "222 47% 11%";
const primary =
rootStyle.getPropertyValue("--primary")?.trim() || "#3b82f6";
const background =
rootStyle.getPropertyValue("--background")?.trim() || "0 0% 100%";
const foreground =
rootStyle.getPropertyValue("--foreground")?.trim() || "222 47% 11%";
const card = rootStyle.getPropertyValue("--card")?.trim() || "0 0% 98%";
const border = rootStyle.getPropertyValue("--border")?.trim() || "214 32% 91%";
const border =
rootStyle.getPropertyValue("--border")?.trim() || "214 32% 91%";
// Convert HSL values to hex format for app compatibility
const hslToHex = (h, s, l) => {
l /= 100;
const a = s * Math.min(l, 1 - l) / 100;
const f = n => {
const a = (s * Math.min(l, 1 - l)) / 100;
const f = (n) => {
const k = (n + h / 30) % 12;
const color = l - a * Math.max(Math.min(k - 3, 9 - k, 1), -1);
return Math.round(255 * color).toString(16).padStart(2, '0');
return Math.round(255 * color)
.toString(16)
.padStart(2, "0");
};
return `#${f(0)}${f(8)}${f(4)}`;
};
const parseHsl = (hslStr) => {
const match = hslStr.match(/(\d+)\s+(\d+)%\s+(\d+)%/);
if (!hslStr) return null;
const match = hslStr
.trim()
.match(/([0-9.]+)\s+([0-9.]+)%\s+([0-9.]+)%/);
if (match) {
return [parseInt(match[1]), parseInt(match[2]), parseInt(match[3])];
return [
parseFloat(match[1]),
parseFloat(match[2]),
parseFloat(match[3]),
];
}
return null;
};
const getContrastYIQ = (hexcolor) => {
if (!hexcolor) return "#ffffff";
hexcolor = hexcolor.replace("#", "");
if (hexcolor.length === 3) {
hexcolor = hexcolor
.split("")
.map((c) => c + c)
.join("");
}
if (hexcolor.length !== 6) return "#ffffff";
var r = parseInt(hexcolor.substr(0, 2), 16);
var g = parseInt(hexcolor.substr(2, 2), 16);
var b = parseInt(hexcolor.substr(4, 2), 16);
var yiq = (r * 299 + g * 587 + b * 114) / 1000;
return yiq >= 128 ? "#000000" : "#ffffff";
};
const bgHsl = parseHsl(background);
const fgHsl = parseHsl(foreground);
const cardHsl = parseHsl(card);
const borderHsl = parseHsl(border);
// Update the app's CSS variables with the theme colors
// These inline styles override the theme-sentient.css values
let calculatedTextHex = "#ffffff";
if (bgHsl) {
const bgHex = hslToHex(...bgHsl);
document.documentElement.style.setProperty("--bg", bgHex);
document.documentElement.style.setProperty("--primary-bg", `hsl(${background})`);
document.documentElement.style.setProperty("--bg-secondary", bgHex);
document.documentElement.style.setProperty(
"--primary-bg",
`hsl(${background})`,
);
document.documentElement.style.setProperty("--header-bg", bgHex);
document.documentElement.style.setProperty("--glass-bg", bgHex);
document.documentElement.style.setProperty("--sidebar-bg", bgHex);
calculatedTextHex = getContrastYIQ(bgHex);
}
if (fgHsl) {
const textHex = hslToHex(...fgHsl);
document.documentElement.style.setProperty("--text", textHex);
document.documentElement.style.setProperty("--primary-fg", `hsl(${foreground})`);
document.documentElement.style.setProperty("--text-primary", textHex);
document.documentElement.style.setProperty(
"--text-secondary",
textHex,
);
document.documentElement.style.setProperty("--text-muted", textHex);
document.documentElement.style.setProperty(
"--primary-fg",
`hsl(${foreground})`,
);
} else if (bgHsl) {
document.documentElement.style.setProperty(
"--text",
calculatedTextHex,
);
document.documentElement.style.setProperty(
"--text-primary",
calculatedTextHex,
);
document.documentElement.style.setProperty(
"--text-secondary",
calculatedTextHex,
);
document.documentElement.style.setProperty(
"--text-muted",
calculatedTextHex,
);
}
if (cardHsl) {
const surfaceHex = hslToHex(...cardHsl);
document.documentElement.style.setProperty("--surface", surfaceHex);
document.documentElement.style.setProperty(
"--surface-hover",
surfaceHex,
);
document.documentElement.style.setProperty(
"--surface-active",
surfaceHex,
);
document.documentElement.style.setProperty("--card-bg", surfaceHex);
}
if (borderHsl) {
const borderHex = hslToHex(...borderHsl);
document.documentElement.style.setProperty("--border", borderHex);
document.documentElement.style.setProperty(
"--border-light",
borderHex,
);
}
// Check if config.csv already set the primary color, we shouldn't wipe it
// Only update color and suggestion variables if they aren't marked as bot-config
if (document.documentElement.getAttribute("data-has-bot-colors") !== "true") {
document.documentElement.style.setProperty("--chat-color1", `hsl(${primary})`);
document.documentElement.style.setProperty("--chat-color2", `hsl(${card})`);
document.documentElement.style.setProperty("--suggestion-color", `hsl(${primary})`);
document.documentElement.style.setProperty("--suggestion-bg", `hsl(${card})`);
document.documentElement.style.setProperty("--color1", `hsl(${primary})`);
document.documentElement.style.setProperty("--color2", `hsl(${card})`);
if (
document.documentElement.getAttribute("data-has-bot-colors") !==
"true"
) {
document.documentElement.style.setProperty(
"--chat-color1",
`hsl(${primary})`,
);
document.documentElement.style.setProperty(
"--chat-color2",
`hsl(${card})`,
);
document.documentElement.style.setProperty(
"--suggestion-color",
`hsl(${primary})`,
);
document.documentElement.style.setProperty(
"--suggestion-bg",
`hsl(${card})`,
);
document.documentElement.style.setProperty(
"--color1",
`hsl(${primary})`,
);
document.documentElement.style.setProperty(
"--color2",
`hsl(${card})`,
);
}
console.log("✓ Theme colors applied:", { bg: background, primary: primary });
console.log("✓ Theme colors applied:", {
bg: background,
primary: primary,
});
updateDropdown();
subscribers.forEach((cb) => cb({ themeId: id, themeName: theme.name }));
}, 50);
@ -173,7 +271,8 @@ const ThemeManager = (() => {
}
function updateDropdown() {
// Dropdown removed
const select = document.getElementById("themeDropdown");
if (select) select.value = currentThemeId;
}
function createDropdown() {
@ -214,9 +313,12 @@ const ThemeManager = (() => {
currentThemeId = saved;
loadTheme(saved);
// Dropdown injection removed
// const container = document.getElementById("themeSelectorContainer");
// if (container) container.appendChild(createDropdown());
// Dropdown injection restored for the window manager
const container = document.getElementById("themeSelectorContainer");
if (container) {
container.innerHTML = "";
container.appendChild(createDropdown());
}
console.log("✓ Theme Manager initialized");
}
@ -233,24 +335,22 @@ const ThemeManager = (() => {
if (data.logo_url) {
// For img elements - set src and show, hide SVG
const logoImg = document.querySelector('.logo-icon-img');
const logoSvg = document.querySelector('.logo-icon-svg');
const logoImg = document.querySelector(".logo-icon-img");
const logoSvg = document.querySelector(".logo-icon-svg");
if (logoImg && logoSvg) {
logoImg.src = data.logo_url;
logoImg.alt = data.title || 'Logo';
logoImg.style.display = 'block';
logoSvg.style.display = 'none';
logoImg.alt = data.title || "Logo";
logoImg.style.display = "block";
logoSvg.style.display = "none";
}
// For elements that use background image
document
.querySelectorAll(".assistant-avatar")
.forEach((el) => {
el.style.backgroundImage = `url("${data.logo_url}")`;
el.style.backgroundSize = "contain";
el.style.backgroundRepeat = "no-repeat";
el.style.backgroundPosition = "center";
});
document.querySelectorAll(".assistant-avatar").forEach((el) => {
el.style.backgroundImage = `url("${data.logo_url}")`;
el.style.backgroundSize = "contain";
el.style.backgroundRepeat = "no-repeat";
el.style.backgroundPosition = "center";
});
}
if (data.color1) {
document.documentElement.style.setProperty("--color1", data.color1);

83
ui/suite/js/vendor/tailwindcss.js vendored Normal file

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,296 @@
if (typeof window.WindowManager === 'undefined') {
class WindowManager {
constructor() {
this.openWindows = [];
this.activeWindowId = null;
this.zIndexCounter = 100;
// Will fetch dynamically in open() since script runs before DOM is ready
this.workspace = null;
this.taskbarApps = null;
}
open(id, title, htmlContent) {
// Lazy load the container elements to avoid head script loading issues
if (!this.workspace) this.workspace = document.getElementById('desktop-content') || document.body;
if (!this.taskbarApps) this.taskbarApps = document.getElementById('taskbar-apps');
// If window already exists, focus it
const existingWindow = this.openWindows.find(w => w.id === id);
if (existingWindow) {
this.focus(id);
return;
}
// Create new window
const windowData = {
id,
title,
isMinimized: false,
isMaximized: false,
previousState: null
};
this.openWindows.push(windowData);
// Generate DOM structure
const windowEl = document.createElement('div');
windowEl.id = `window-${id}`;
// Add random slight offset for cascade effect
const offset = (this.openWindows.length * 20) % 100;
const top = 100 + offset;
const left = 150 + offset;
windowEl.className = 'window-element';
windowEl.style.top = `${top}px`;
windowEl.style.left = `${left}px`;
windowEl.style.zIndex = this.zIndexCounter++;
windowEl.innerHTML = `
<!-- Header (Draggable) -->
<div class="window-header">
<div class="font-mono text-xs font-bold text-brand-600 tracking-wide">${title}</div>
<div class="flex space-x-3 text-gray-400">
<button class="btn-minimize hover:text-gray-600" onclick="window.WindowManager.toggleMinimize('${id}')"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="5" y1="12" x2="19" y2="12"></line></svg></button>
<button class="btn-maximize hover:text-gray-600" onclick="window.WindowManager.toggleMaximize('${id}')"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect></svg></button>
<button class="btn-close hover:text-red-500" onclick="window.WindowManager.close('${id}')"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg></button>
</div>
</div>
<!-- Body (HTMX target) -->
<div id="window-body-${id}" class="window-body relative flex-1 overflow-y-auto bg-[#fafdfa]"></div>
`;
this.workspace.appendChild(windowEl);
// Inject content into the window body
const windowBody = windowEl.querySelector(`#window-body-${id}`);
if (windowBody) {
this.injectContentWithScripts(windowBody, htmlContent);
}
// Add to taskbar
if (this.taskbarApps) {
const taskbarIcon = document.createElement('div');
taskbarIcon.id = `taskbar-item-${id}`;
taskbarIcon.className = 'taskbar-item taskbar-icon';
taskbarIcon.onclick = () => this.toggleMinimize(id);
let iconHtml = '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect></svg>';
if (id === 'vibe') iconHtml = '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 2v20M17 5H9.5a3.5 3.5 0 0 0 0 7h5a3.5 3.5 0 0 1 0 7H6"/></svg>';
else if (id === 'tasks') iconHtml = '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M9 11l3 3L22 4"/><path d="M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11"/></svg>';
else if (id === 'chat') iconHtml = '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg>';
else if (id === 'terminal') iconHtml = '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="4 17 10 11 4 5"/><line x1="12" y1="19" x2="20" y2="19"/></svg>';
else if (id === 'drive') iconHtml = '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"/></svg>';
else if (id === 'editor') iconHtml = '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="16 18 22 12 16 6"/><polyline points="8 6 2 12 8 18"/></svg>';
else if (id === 'browser') iconHtml = '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><polygon points="16.24 7.76 14.12 14.12 7.76 16.24 9.88 9.88 16.24 7.76"/></svg>';
else if (id === 'mail') iconHtml = '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z"></path><polyline points="22,6 12,13 2,6"></polyline></svg>';
else if (id === 'settings') iconHtml = '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="3"></circle><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"></path></svg>';
taskbarIcon.innerHTML = `
<div class="app-icon w-8 h-8 rounded-md flex items-center justify-center text-xs shadow-sm" style="color: var(--text, #374151);">
${iconHtml}
</div>
`;
this.taskbarApps.appendChild(taskbarIcon);
}
this.makeDraggable(windowEl);
this.makeResizable(windowEl);
this.focus(id);
// Tell HTMX to process the new content
if (window.htmx) {
htmx.process(windowEl);
}
}
focus(id) {
this.activeWindowId = id;
const windowEl = document.getElementById(`window-${id}`);
if (windowEl) {
windowEl.style.zIndex = this.zIndexCounter++;
}
// Highlight taskbar icon
if (this.taskbarApps) {
const icons = this.taskbarApps.querySelectorAll('.taskbar-icon');
icons.forEach(icon => {
icon.classList.remove('');
icon.classList.add('border-transparent');
});
const activeIcon = document.getElementById(`taskbar-item-${id}`);
if (activeIcon) {
activeIcon.classList.remove('border-transparent');
activeIcon.classList.add('');
}
}
}
close(id) {
const windowEl = document.getElementById(`window-${id}`);
if (windowEl) {
windowEl.remove();
}
const taskbarIcon = document.getElementById(`taskbar-item-${id}`);
if (taskbarIcon) {
taskbarIcon.remove();
}
this.openWindows = this.openWindows.filter(w => w.id !== id);
if (this.activeWindowId === id) {
this.activeWindowId = null;
// Optionally focus the next highest z-index window
}
}
toggleMinimize(id) {
const windowObj = this.openWindows.find(w => w.id === id);
if (!windowObj) return;
const windowEl = document.getElementById(`window-${id}`);
if (!windowEl) return;
if (windowObj.isMinimized) {
// Restore
windowEl.style.display = 'flex';
windowObj.isMinimized = false;
this.focus(id);
} else {
// Minimize
windowEl.style.display = 'none';
windowObj.isMinimized = true;
if (this.activeWindowId === id) {
this.activeWindowId = null;
}
}
}
toggleMaximize(id) {
const windowObj = this.openWindows.find(w => w.id === id);
if (!windowObj) return;
const windowEl = document.getElementById(`window-${id}`);
if (!windowEl) return;
if (windowObj.isMaximized) {
// Restore
windowEl.style.width = windowObj.previousState.width;
windowEl.style.height = windowObj.previousState.height;
windowEl.style.top = windowObj.previousState.top;
windowEl.style.left = windowObj.previousState.left;
windowObj.isMaximized = false;
} else {
// Maximize
windowObj.previousState = {
width: windowEl.style.width,
height: windowEl.style.height,
top: windowEl.style.top,
left: windowEl.style.left
};
// Adjust for taskbar height (assuming taskbar is at bottom)
const taskbarHeight = document.getElementById('taskbar') ? document.getElementById('taskbar').offsetHeight : 0;
windowEl.style.width = '100%';
windowEl.style.height = `calc(100% - ${taskbarHeight}px)`;
windowEl.style.top = '0px';
windowEl.style.left = '0px';
windowObj.isMaximized = true;
}
this.focus(id);
}
makeDraggable(windowEl) {
const header = windowEl.querySelector('.window-header');
if (!header) return;
let isDragging = false;
let startX, startY, initialLeft, initialTop;
const onMouseDown = (e) => {
// Don't drag if clicking buttons
if (e.target.tagName.toLowerCase() === 'button' || e.target.closest('button')) return;
isDragging = true;
startX = e.clientX;
startY = e.clientY;
initialLeft = parseInt(windowEl.style.left || 0, 10);
initialTop = parseInt(windowEl.style.top || 0, 10);
this.focus(windowEl.id.replace('window-', ''));
document.addEventListener('mousemove', onMouseMove);
document.addEventListener('mouseup', onMouseUp);
};
const onMouseMove = (e) => {
if (!isDragging) return;
// Allow animation frame optimization here in a real implementation
requestAnimationFrame(() => {
const dx = e.clientX - startX;
const dy = e.clientY - startY;
// Add basic boundaries
let newLeft = initialLeft + dx;
let newTop = initialTop + dy;
// Prevent dragging completely out
newTop = Math.max(0, newTop);
windowEl.style.left = `${newLeft}px`;
windowEl.style.top = `${newTop}px`;
});
};
const onMouseUp = () => {
isDragging = false;
document.removeEventListener('mousemove', onMouseMove);
document.removeEventListener('mouseup', onMouseUp);
};
header.addEventListener('mousedown', onMouseDown);
// Add focus listener to the whole window
windowEl.addEventListener('mousedown', () => {
this.focus(windowEl.id.replace('window-', ''));
});
}
injectContentWithScripts(container, htmlContent) {
// Create a temporary div to parse the HTML
const tempDiv = document.createElement('div');
tempDiv.innerHTML = htmlContent;
// Extract all script tags
const scripts = tempDiv.querySelectorAll('script');
const scriptsToExecute = [];
scripts.forEach((originalScript) => {
const scriptClone = document.createElement('script');
Array.from(originalScript.attributes).forEach(attr => {
scriptClone.setAttribute(attr.name, attr.value);
});
scriptClone.textContent = originalScript.textContent;
scriptsToExecute.push(scriptClone);
originalScript.remove(); // Remove from tempDiv so innerHTML doesn't include it
});
// Inject HTML content without scripts
container.innerHTML = tempDiv.innerHTML;
// Execute each script
scriptsToExecute.forEach((script) => {
container.appendChild(script);
});
}
makeResizable(windowEl) {
// Implement simple bottom-right resize for now
// In a full implementation, you'd add invisible handles
windowEl.style.resize = 'both';
// Note: CSS resize creates conflicts with custom dragging/resizing if not careful.
// For a true "WinBox" feel, custom handles (divs) on all 8 edges/corners are needed.
}
}
// Initialize globally
window.WindowManager = new WindowManager();
}

1308
ui/suite/partials/chat.html Normal file

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,418 @@
<style>
/* CRITICAL: Overriding default Tailwind resets that break the layout */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
margin: 0 !important;
padding: 0 !important;
font-family: 'Fira Code', 'Fira Sans', Arial, sans-serif !important;
background: var(--bg) !important;
overflow: hidden !important;
height: 100vh !important;
width: 100vw !important;
display: flex !important;
}
/* Core Layout replicating BUILD V3 screenshot styling */
.build-container {
width: 100%;
height: 100%;
display: flex;
position: absolute;
inset: 0;
z-index: 1000;
background: var(--bg);
}
/* Left Sidebar */
.sidebar {
width: 51px;
height: 100%;
background: var(--surface);
border-right: 1px solid var(--border);
display: flex;
flex-direction: column;
z-index: 100;
}
.sidebar-item {
width: 51px;
height: 50px;
border-bottom: 1px solid var(--border);
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: all 0.15s ease;
position: relative;
}
.sidebar-item:hover {
background: var(--surface-hover, #ffffff);
}
.sidebar-item.active {
background: var(--surface-hover, #ffffff);
border-left: 3px solid var(--accent);
}
.sidebar-icon {
width: 30px;
height: 30px;
opacity: 0.6;
}
.sidebar-item:hover .sidebar-icon {
opacity: 1;
}
/* Main Content wrapper */
.main-wrapper {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
position: relative;
}
/* Top Navigation Tabs */
.tabs-container {
display: flex;
flex-direction: column;
background: var(--surface);
border-bottom: 1px solid var(--border);
z-index: 100;
}
.tabs-row {
display: flex;
height: 34px;
}
.main-tab {
height: 34px;
min-width: 169px;
flex: 1;
border-right: 1px solid var(--border);
display: flex;
align-items: center;
padding: 0 18px;
cursor: pointer;
position: relative;
}
.main-tab:hover {
background: var(--surface-hover, #ffffff);
}
.main-tab.active {
background: var(--accent);
border-color: var(--accent);
}
.main-tab.active .main-tab-content {
color: var(--bg);
}
.main-tab-content {
display: flex;
align-items: center;
gap: 4px;
font-family: 'Fira Code', monospace;
font-size: 14px;
font-weight: 500;
color: var(--text);
}
/* Workspace (Where windows float) */
.workspace {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
background: var(--bg);
position: relative;
z-index: 10;
}
/* The Panel Grid (Desktop Icons) */
.panel-section {
flex: 1;
padding: 26px 33px;
overflow-y: auto;
z-index: 10;
position: absolute;
inset: 0;
pointer-events: none;
}
.panel-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 20px;
max-width: 1200px;
pointer-events: auto;
}
/* Interactive Desktop Icons triggering HTMX */
.desktop-icon {
background: var(--bg);
border: 1px solid var(--border);
border-radius: 8px;
height: 67px;
padding: 10px;
display: flex;
flex-direction: column;
justify-content: flex-end;
align-items: flex-start;
cursor: pointer;
transition: all 0.2s ease;
position: relative;
width: 100%;
}
.desktop-icon:hover {
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
transform: translateY(-2px);
border-color: var(--accent);
}
.panel-card-icon {
position: absolute;
top: -20px;
left: 10px;
width: 42px;
height: 42px;
}
.panel-card-label {
font-family: 'Fira Code', monospace;
font-size: 13px;
font-weight: 500;
color: var(--text);
margin-top: auto;
}
.bg-grid { display: none; }
.bg-svg { display: none !important; }
/* Bottom Taskbar */
.toolbar {
height: 50px;
background: var(--bg);
border-top: 1px solid var(--border);
display: flex;
align-items: center;
padding: 0 8px;
z-index: 100;
position: relative;
}
#taskbar-apps {
display: flex;
flex: 1;
height: 100%;
align-items: center;
gap: 0px;
}
.toolbar-time {
font-family: 'Fira Code', monospace;
font-size: 14px;
color: var(--text);
text-align: right;
line-height: 1.4;
padding: 0 10px;
margin-left: auto;
}
/* Taskbar Items generated by WindowManager */
.taskbar-item {
width: 40px;
height: 40px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: all 0.15s ease;
border-bottom: 2px solid transparent;
}
.taskbar-item:hover {
background: var(--surface);
}
.taskbar-item.active {
border-bottom-color: var(--accent);
background: linear-gradient(to bottom, transparent 50%, var(--accent-light, rgba(132, 214, 105, 0.1)) 100%);
}
/* Utility */
svg {
display: block;
}
</style>
<div class="build-container">
<!-- Left Sidebar -->
<aside class="sidebar">
<div class="sidebar-item active" title="Home">
<svg class="sidebar-icon" viewBox="0 0 24 24" fill="none" stroke="var(--text)" stroke-width="2">
<path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"></path>
<polyline points="9 22 9 12 15 12 15 22"></polyline>
</svg>
</div>
<div class="sidebar-item" title="Terminal">
<svg class="sidebar-icon" viewBox="0 0 24 24" fill="none" stroke="var(--text)" stroke-width="2">
<polyline points="4 17 10 11 4 5"></polyline>
<line x1="12" y1="19" x2="20" y2="19"></line>
</svg>
</div>
</aside>
<!-- Main Wrapper -->
<div class="main-wrapper">
<!-- Top Navigation Tabs -->
<div class="tabs-container">
<div class="tabs-row">
<div class="main-tab active">
<div class="main-tab-content"><span>//</span><span>BUILD</span></div>
</div>
<div class="main-tab">
<div class="main-tab-content"><span>//</span><span>REVIEW</span></div>
</div>
<div class="main-tab">
<div class="main-tab-content"><span>//</span><span>DEPLOY</span></div>
</div>
<div class="main-tab">
<div class="main-tab-content"><span>//</span><span>MONITOR</span></div>
</div>
</div>
</div>
<!-- Workspace container where WindowManager operates -->
<div class="workspace" id="desktop-content-inner">
<!-- Background Pattern -->
<div class="panel-section">
<div class="panel-grid">
<!-- HTMX Enabled Desktop Icons that WindowManager catches -->
<div class="desktop-icon" data-app-id="chat" data-app-title="Chat"
hx-get="/suite/partials/chat.html" hx-swap="none">
<svg class="panel-card-icon" viewBox="0 0 42 42" fill="none">
<circle cx="21" cy="21" r="20" stroke="var(--accent)" stroke-width="2" />
<path d="M14 21h14M21 14v14" stroke="var(--accent)" stroke-width="2" />
</svg>
<div class="panel-card-label">Chat</div>
</div>
<div class="desktop-icon" data-app-id="vibe" data-app-title="Vibe"
hx-get="/suite/partials/vibe.html" hx-swap="none">
<svg class="panel-card-icon" viewBox="0 0 42 42" fill="none">
<circle cx="21" cy="21" r="20" stroke="var(--accent)" stroke-width="2" />
<path d="M12 14l8 7-8 7" stroke="var(--accent)" stroke-width="2" stroke-linecap="round"
stroke-linejoin="round" />
<line x1="22" y1="28" x2="32" y2="28" stroke="var(--accent)" stroke-width="2"
stroke-linecap="round" />
</svg>
<div class="panel-card-label">Vibe</div>
</div>
<div class="desktop-icon" data-app-id="tasks" data-app-title="Tasks"
hx-get="/suite/tasks/task-window.html" hx-swap="none">
<svg class="panel-card-icon" viewBox="0 0 42 42" fill="none">
<rect x="2" y="2" width="38" height="38" rx="4" stroke="var(--text)" stroke-width="2" />
<line x1="12" y1="12" x2="30" y2="12" stroke="var(--text)" stroke-width="2" />
<line x1="12" y1="21" x2="30" y2="21" stroke="var(--text)" stroke-width="2" />
<line x1="12" y1="30" x2="24" y2="30" stroke="var(--text)" stroke-width="2" />
</svg>
<div class="panel-card-label">Tasks</div>
</div>
<div class="desktop-icon" data-app-id="terminal" data-app-title="Terminal"
hx-get="/suite/partials/terminal.html" hx-swap="none">
<svg class="panel-card-icon" viewBox="0 0 42 42" fill="none">
<rect x="2" y="4" width="38" height="34" rx="4" stroke="var(--text)" stroke-width="2" />
<line x1="10" y1="14" x2="32" y2="14" stroke="var(--text)" stroke-width="2" />
<line x1="10" y1="22" x2="28" y2="22" stroke="var(--text)" stroke-width="2" />
<line x1="10" y1="30" x2="24" y2="30" stroke="var(--text)" stroke-width="2" />
</svg>
<div class="panel-card-label">Terminal</div>
</div>
<div class="desktop-icon" data-app-id="explorer" data-app-title="Explorer"
hx-get="/suite/partials/explorer.html" hx-swap="none">
<svg class="panel-card-icon" viewBox="0 0 42 42" fill="none">
<rect x="2" y="2" width="38" height="38" rx="4" stroke="var(--text)" stroke-width="2" />
<line x1="10" y1="10" x2="32" y2="10" stroke="var(--text)" stroke-width="2" />
<line x1="10" y1="18" x2="28" y2="18" stroke="var(--text)" stroke-width="2" />
<line x1="10" y1="26" x2="32" y2="26" stroke="var(--text)" stroke-width="2" />
<line x1="10" y1="34" x2="24" y2="34" stroke="var(--text)" stroke-width="2" />
</svg>
<div class="panel-card-label">Explorer</div>
</div>
<div class="desktop-icon" data-app-id="editor" data-app-title="Editor"
hx-get="/suite/partials/editor.html" hx-swap="none">
<svg class="panel-card-icon" viewBox="0 0 42 42" fill="none">
<polyline points="4 8 12 2 20 8" stroke="var(--text)" stroke-width="2" />
<line x1="12" y1="2" x2="12" y2="24" stroke="var(--text)" stroke-width="2" />
<polyline points="22 16 30 10 38 16" stroke="var(--text)" stroke-width="2" />
<line x1="30" y1="10" x2="30" y2="32" stroke="var(--text)" stroke-width="2" />
</svg>
<div class="panel-card-label">Editor</div>
</div>
<div class="desktop-icon" data-app-id="browser" data-app-title="Browser"
hx-get="/suite/partials/browser.html" hx-swap="none">
<svg class="panel-card-icon" viewBox="0 0 42 42" fill="none">
<rect x="2" y="4" width="38" height="34" rx="4" stroke="var(--text)" stroke-width="2" />
<circle cx="14" cy="16" r="4" stroke="var(--text)" stroke-width="2" />
<path d="M6 34a6 6 0 0 1 6-6h10a6 6 0 0 1 6 6v2H6v-2z" stroke="var(--text)" stroke-width="2" />
</svg>
<div class="panel-card-label">Browser</div>
</div>
</div>
</div>
</div>
<!-- Bottom Taskbar -->
<footer class="toolbar" id="taskbar">
<div id="taskbar-apps">
<!-- Taskbar items populated automatically by window-manager.js -->
</div>
<div class="toolbar-time" style="display: flex; align-items: center; gap: 15px;">
<div id="themeSelectorContainer"></div>
<div style="text-align: right;">
<div id="clock-time">00:00</div>
<div id="clock-date">01/01/2026</div>
</div>
</div>
</footer>
</div>
</div>
<!-- HTMX Intercepts and WindowManager Init as described in UI.md Phase 3 -->
<script>
// Simple Clock implementation matching the screenshot bottom right corner
setInterval(() => {
const now = new Date();
const timeEl = document.getElementById('clock-time');
const dateEl = document.getElementById('clock-date');
if (timeEl) timeEl.textContent = now.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
if (dateEl) dateEl.textContent = now.toLocaleDateString();
}, 1000);
</script>

View file

@ -0,0 +1,674 @@
<!-- Editor - General Bots (Code & Text Editor) -->
<style>
/* Editor uses global theme variables from base.css */
.editor-container {
display: flex;
flex-direction: column;
height: calc(100vh - 64px);
background: var(--bg, #0f172a);
color: var(--text, #f8fafc);
}
.editor-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 12px 20px;
background: var(--surface, #1e293b);
border-bottom: 1px solid var(--border, #334155);
}
.editor-title {
display: flex;
align-items: center;
gap: 12px;
}
.editor-title-icon {
font-size: 24px;
}
.editor-title-text {
font-size: 16px;
font-weight: 600;
}
.editor-path {
font-size: 12px;
color: var(--text-secondary, #94a3b8);
}
.editor-toolbar {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 20px;
background: var(--surface, #1e293b);
border-bottom: 1px solid var(--border, #334155);
}
.toolbar-group {
display: flex;
align-items: center;
gap: 4px;
padding-right: 12px;
border-right: 1px solid var(--border, #334155);
}
.toolbar-group:last-child {
border-right: none;
}
.btn {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 8px 14px;
border: none;
border-radius: 6px;
font-size: 13px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s;
background: var(--surface-hover, #334155);
color: var(--text, #f8fafc);
}
.btn:hover {
background: var(--border-light, #475569);
}
.btn-primary {
background: var(--primary, #3b82f6);
color: white;
}
.btn-primary:hover {
background: var(--primary-hover, #2563eb);
}
.btn-magic {
background: linear-gradient(135deg, #8b5cf6, #3b82f6);
color: white;
border: none;
}
.btn-magic:hover {
background: linear-gradient(135deg, #3b82f6, #8b5cf6);
transform: scale(1.02);
}
.magic-panel {
position: fixed;
right: 20px;
bottom: 60px;
width: 400px;
max-height: 500px;
background: var(--surface, #1e293b);
border: 1px solid var(--border, #334155);
border-radius: 12px;
box-shadow: 0 8px 32px rgba(0,0,0,0.4);
z-index: 1000;
display: none;
flex-direction: column;
overflow: hidden;
}
.magic-panel.visible {
display: flex;
}
.magic-header {
padding: 16px;
border-bottom: 1px solid var(--border, #334155);
display: flex;
align-items: center;
justify-content: space-between;
background: linear-gradient(135deg, rgba(139,92,246,0.1), rgba(59,130,246,0.1));
}
.magic-header h3 {
font-size: 14px;
font-weight: 600;
display: flex;
align-items: center;
gap: 8px;
}
.magic-close {
background: none;
border: none;
color: var(--text-secondary, #94a3b8);
cursor: pointer;
font-size: 18px;
}
.magic-content {
flex: 1;
overflow-y: auto;
padding: 16px;
}
.magic-loading {
text-align: center;
padding: 40px;
color: var(--text-secondary, #94a3b8);
}
.magic-result {
background: var(--surface-hover, #334155);
border-radius: 8px;
padding: 12px;
margin-bottom: 12px;
}
.magic-result pre {
font-family: monospace;
font-size: 12px;
white-space: pre-wrap;
overflow-x: auto;
}
.magic-apply-btn {
background: var(--success);
color: white;
border: none;
padding: 8px 16px;
border-radius: 6px;
cursor: pointer;
margin-top: 12px;
}
.btn-small {
padding: 6px 10px;
font-size: 12px;
}
.editor-content {
flex: 1;
display: flex;
overflow: hidden;
}
.editor-wrapper {
display: flex;
flex: 1;
overflow: hidden;
}
.line-numbers {
width: 50px;
background: var(--surface, #1e293b);
border-right: 1px solid var(--border, #334155);
padding: 16px 8px;
overflow: hidden;
text-align: right;
user-select: none;
font-family: "Consolas", monospace;
font-size: 13px;
line-height: 1.6;
color: var(--text-secondary, #94a3b8);
}
.text-editor {
flex: 1;
background: var(--bg, #0f172a);
color: var(--text, #f8fafc);
border: none;
padding: 16px;
font-family: "Consolas", monospace;
font-size: 13px;
line-height: 1.6;
resize: none;
outline: none;
white-space: pre;
overflow: auto;
tab-size: 4;
}
.csv-editor {
flex: 1;
overflow: auto;
padding: 16px;
}
.csv-table {
width: 100%;
border-collapse: collapse;
font-size: 13px;
}
.csv-table th,
.csv-table td {
border: 1px solid var(--border, #334155);
padding: 0;
min-width: 120px;
}
.csv-table th {
background: var(--surface-hover, #334155);
font-weight: 600;
}
.csv-table .row-num {
width: 40px;
min-width: 40px;
background: var(--surface, #1e293b);
color: var(--text-secondary, #94a3b8);
text-align: center;
padding: 8px 4px;
font-size: 12px;
}
.csv-input {
width: 100%;
background: transparent;
border: none;
color: var(--text, #f8fafc);
padding: 8px 12px;
font-size: 13px;
outline: none;
}
.csv-input:focus {
background: var(--surface, #1e293b);
}
.status-bar {
display: flex;
align-items: center;
justify-content: space-between;
padding: 6px 16px;
background: var(--surface, #1e293b);
border-top: 1px solid var(--border, #334155);
font-size: 12px;
color: var(--text-secondary, #94a3b8);
}
.status-left,
.status-right {
display: flex;
align-items: center;
gap: 16px;
}
.dirty-indicator {
width: 8px;
height: 8px;
background: var(--warning);
border-radius: 50%;
margin-left: 8px;
}
.htmx-indicator {
display: none;
}
.htmx-request .htmx-indicator {
display: inline-block;
}
.spinner {
width: 14px;
height: 14px;
border: 2px solid transparent;
border-top-color: currentColor;
border-radius: 50%;
animation: spin 0.8s linear infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
.notification {
position: fixed;
bottom: 60px;
right: 20px;
padding: 12px 20px;
background: var(--surface-hover, #334155);
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
border-left: 4px solid var(--primary, #3b82f6);
opacity: 0;
transform: translateX(100%);
transition: all 0.3s ease;
}
.notification.show {
opacity: 1;
transform: translateX(0);
}
.notification.success {
border-left-color: var(--success);
}
.notification.error {
border-left-color: var(--error);
}
</style>
<div class="editor-container">
<!-- Header -->
<div class="editor-header">
<div class="editor-title">
<span class="editor-title-icon">📝</span>
<div>
<span
class="editor-title-text"
id="editor-filename"
hx-get="/api/editor/filename"
hx-trigger="load"
hx-swap="innerHTML"></div>
Untitled
</span>
<div
class="editor-path"
id="editor-filepath"
hx-get="/api/editor/filepath"
hx-trigger="load"
hx-swap="innerHTML">
</div>
</div>
<span
class="dirty-indicator"
id="dirty-indicator"
style="display: none;"
title="Unsaved changes">
</span>
</div>
<div>
<a href="#drive"
class="btn btn-small"
hx-get="/api/drive/list"
hx-target="closest .window-body #main-content"
hx-push-url="true">
✕ Close
</a>
</div>
</div>
<!-- Toolbar -->
<div class="editor-toolbar">
<div class="toolbar-group">
<button
class="btn btn-primary btn-small"
hx-post="/api/editor/save"
hx-include="#text-editor"
hx-indicator="#save-spinner"
hx-swap="none"
hx-on::after-request="showSaveNotification(event)">
<span class="htmx-indicator spinner" id="save-spinner"></span>
💾 Save
</button>
<button
class="btn btn-small"
hx-get="/api/editor/save-as"
hx-target="closest .window-body #save-dialog"
hx-swap="innerHTML">
Save As
</button>
</div>
<div class="toolbar-group">
<button
class="btn btn-small"
hx-post="/api/editor/undo"
hx-target="closest .window-body #editor-content"
hx-swap="innerHTML">
↩️ Undo
</button>
<button
class="btn btn-small"
hx-post="/api/editor/redo"
hx-target="closest .window-body #editor-content"
hx-swap="innerHTML">
↪️ Redo
</button>
</div>
<div class="toolbar-group" id="text-tools">
<button
class="btn btn-small"
hx-post="/api/editor/format"
hx-include="#text-editor"
hx-target="closest .window-body #text-editor"
hx-swap="innerHTML">
{ } Format
</button>
<button
class="btn btn-small btn-magic"
onclick="showMagicPanel()"
title="AI Improvements (Ctrl+M)">
✨ Magic
</button>
</div>
<div class="toolbar-group" id="csv-tools" style="display: none;">
<button
class="btn btn-small"
hx-post="/api/editor/csv/add-row"
hx-target="closest .window-body #csv-table-div"
hx-swap="beforeend">
Row
</button>
<button
class="btn btn-small"
hx-post="/api/editor/csv/add-column"
hx-target="closest .window-body #csv-editor"
hx-swap="innerHTML">
Column
</button>
</div>
</div>
<!-- Magic AI Panel -->
<div class="magic-panel" id="magic-panel">
<div class="magic-header">
<h3>✨ AI Improvements</h3>
<button class="magic-close" onclick="hideMagicPanel()">×</button>
</div>
<div class="magic-content" id="magic-content">
<div class="magic-loading">Analyzing code...</div>
</div>
</div>
<!-- Editor Content - loaded via HTMX based on file type -->
<div class="editor-content" id="editor-content">
<!-- Text Editor (default) -->
<div class="editor-wrapper" id="text-editor-wrapper">
<div
class="line-numbers"
id="line-numbers"
hx-get="/api/editor/line-numbers"
hx-trigger="keyup from:#text-editor delay:100ms"
hx-swap="innerHTML">
1
</div>
<textarea
class="text-editor"
id="text-editor"
name="content"
spellcheck="false"
hx-post="/api/editor/autosave"
hx-trigger="keyup changed delay:5s"
hx-swap="none"
hx-indicator="#autosave-indicator"
placeholder="Start typing or open a file..."></textarea>
</div>
<!-- CSV Editor (shown for .csv files) -->
<div class="csv-editor" id="csv-editor" style="display: none;">
<table class="csv-table">
<thead id="csv-table-head">
<tr>
<th class="row-num">#</th>
<th>
<input
type="text"
class="csv-input"
name="header_0"
value="Column 1"
hx-post="/api/editor/csv/update-header"
hx-trigger="change"
hx-swap="none">
</th>
</tr>
</thead>
<tdiv
id="csv-table-div"
hx-get="/api/editor/csv/rows"
hx-trigger="load"
hx-swap="innerHTML">
</tdiv>
</table>
</div>
</div>
<!-- Status Bar -->
<div class="status-bar">
<div class="status-left">
<span
id="file-type"
hx-get="/api/editor/filetype"
hx-trigger="load"
hx-swap="innerHTML">
📄 Plain Text
</span>
<span>UTF-8</span>
<span
id="autosave-indicator"
class="htmx-indicator"
style="font-size: 11px;">
Saving...
</span>
</div>
<div class="status-right">
<span
id="cursor-position"
hx-get="/api/editor/position"
hx-trigger="click from:#text-editor, keyup from:#text-editor"
hx-swap="innerHTML">
Ln 1, Col 1
</span>
</div>
</div>
</div>
<!-- Save Dialog (loaded via HTMX) -->
<div id="save-dialog"></div>
<!-- Notification -->
<div class="notification" id="notification"></div>
<script>
// Minimal JS for notification display (could be replaced with htmx extension)
function showSaveNotification(event) {
const notification = document.getElementById('notification');
if (event.detail.successful) {
notification.textContent = '✓ File saved';
notification.className = 'notification success show';
document.getElementById('dirty-indicator').style.display = 'none';
} else {
notification.textContent = '✗ Save failed';
notification.className = 'notification error show';
}
setTimeout(() => notification.classList.remove('show'), 3000);
}
// Mark as dirty on edit
document.getElementById('text-editor')?.addEventListener('input', function() {
document.getElementById('dirty-indicator').style.display = 'inline-block';
});
// Keyboard shortcuts using htmx triggers
document.addEventListener('keydown', function(e) {
if ((e.ctrlKey || e.metaKey) && e.key === 's') {
e.preventDefault();
htmx.trigger(document.querySelector('[hx-post="/api/editor/save"]'), 'click');
}
});
function showMagicPanel() {
document.getElementById('magic-panel').classList.add('visible');
runMagicAnalysis();
}
function hideMagicPanel() {
document.getElementById('magic-panel').classList.remove('visible');
}
async function runMagicAnalysis() {
const content = document.getElementById('magic-content');
const code = document.getElementById('text-editor').value;
if (!code.trim()) {
content.innerHTML = '<p style="color:var(--text-secondary, #94a3b8);text-align:center;padding:40px;">No code to analyze. Start typing or open a file.</p>';
return;
}
content.innerHTML = '<div class="magic-loading">✨ Analyzing your code...</div>';
try {
const response = await fetch('/api/editor/magic', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
div: JSON.stringify({ code: code })
});
if (response.ok) {
const result = await response.json();
renderMagicResult(result);
} else {
content.innerHTML = '<p style="color:var(--error);padding:20px;">Failed to analyze. Try again.</p>';
}
} catch (e) {
content.innerHTML = '<p style="color:var(--error);padding:20px;">Error connecting to AI service.</p>';
}
}
function renderMagicResult(result) {
const content = document.getElementById('magic-content');
if (result.improved_code) {
content.innerHTML = `
<div class="magic-result">
<p><strong>Suggested improvements:</strong></p>
<p style="color:var(--text-secondary, #94a3b8);margin:8px 0;">${result.explanation || 'Improved code structure and patterns.'}</p>
<pre>${escapeHtml(result.improved_code)}</pre>
<button class="magic-apply-btn" onclick="applyMagicCode()">Apply Changes</button>
</div>
`;
window.magicImprovedCode = result.improved_code;
} else if (result.suggestions) {
content.innerHTML = result.suggestions.map(s => `
<div class="magic-result">
<p><strong>${s.title}</strong></p>
<p style="color:var(--text-secondary, #94a3b8);">${s.description}</p>
</div>
`).join('');
} else {
content.innerHTML = '<p style="padding:20px;">Your code looks good! No suggestions at this time.</p>';
}
}
function applyMagicCode() {
if (window.magicImprovedCode) {
document.getElementById('text-editor').value = window.magicImprovedCode;
hideMagicPanel();
}
}
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
document.addEventListener('keydown', (e) => {
if (e.ctrlKey && e.key === 'm') {
e.preventDefault();
showMagicPanel();
}
});
</script>

File diff suppressed because it is too large Load diff

1347
ui/suite/partials/mail.html Normal file

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,73 @@
<!-- Minibar Component — Top-left navigation -->
<style>
.minibar {
position: fixed;
top: 0;
left: 0;
right: 0;
height: 28px;
background: #f8f8f8;
border-bottom: 1px solid #f0f1f2;
display: flex;
align-items: center;
padding: 0 12px;
z-index: 9999;
font-family: 'Fira Code', monospace;
}
.minibar-brand {
display: flex;
align-items: center;
gap: 6px;
font-size: 12px;
font-weight: 700;
color: #3b3b3b;
}
.minibar-brand-icon {
width: 16px;
height: 16px;
}
.minibar-actions {
display: flex;
align-items: center;
gap: 4px;
margin-left: auto;
}
.minibar-btn {
width: 24px;
height: 24px;
border: none;
border-radius: 4px;
background: transparent;
color: #888;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.15s;
font-size: 13px;
padding: 0;
}
.minibar-btn:hover {
background: #e0e0e0;
color: #3b3b3b;
}
</style>
<div class="minibar" id="minibar">
<div class="minibar-brand">
<svg class="minibar-brand-icon" viewBox="0 0 24 24" fill="none" stroke="#84d669" stroke-width="2">
<circle cx="12" cy="12" r="10" />
<path d="M8 12l3 3 5-5" />
</svg>
<span>Agent Farm</span>
</div>
<div class="minibar-actions">
<button class="minibar-btn" title="Settings" type="button"></button>
<button class="minibar-btn" title="Account" type="button">👤</button>
</div>
</div>

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,317 @@
<link rel="stylesheet" href="/suite/tasks/tasks-scoped.css" />
<script src="/suite/tasks/tasks-scoped.js"></script>
<!-- =============================================================================
TASKS APP - Autonomous Task Management
Respects Theme Manager - No hardcoded theme
============================================================================= -->
<div class="tasks-app">
<!-- Hidden element to load stats on page load -->
<div
hx-get="/api/ui/tasks/stats"
hx-trigger="load, taskCreated from:div"
hx-swap="innerHTML"
style="display: none"
></div>
<!-- Status Filter Pills Row -->
<div class="status-filter-row">
<button
class="filter-pill"
data-filter="complete"
hx-get="/api/ui/tasks?filter=complete"
hx-target="closest .window-body #task-list"
hx-swap="innerHTML"
>
<span class="pill-icon"></span>
<span class="pill-label" data-i18n="tasks-completed">Complete</span>
<span class="pill-count" id="count-complete">-</span>
</button>
<button
class="filter-pill active"
data-filter="all"
hx-get="/api/ui/tasks?filter=all"
hx-target="closest .window-body #task-list"
hx-swap="innerHTML"
>
<span class="pill-icon">📋</span>
<span class="pill-label" data-i18n="tasks-all">All Tasks</span>
<span class="pill-count" id="count-all">-</span>
</button>
<button
class="filter-pill"
data-filter="active"
hx-get="/api/ui/tasks?filter=active"
hx-target="closest .window-body #task-list"
hx-swap="innerHTML"
>
<span class="pill-icon"></span>
<span class="pill-label" data-i18n="tasks-active"
>Active Intents</span
>
<span class="pill-count" id="count-active">-</span>
</button>
<button
class="filter-pill"
data-filter="awaiting"
hx-get="/api/ui/tasks?filter=awaiting"
hx-target="closest .window-body #task-list"
hx-swap="innerHTML"
>
<span class="pill-icon"></span>
<span class="pill-label" data-i18n="tasks-awaiting"
>Awaiting Decision</span
>
<span class="pill-count" id="count-awaiting">-</span>
</button>
<button
class="filter-pill"
data-filter="paused"
hx-get="/api/ui/tasks?filter=paused"
hx-target="closest .window-body #task-list"
hx-swap="innerHTML"
>
<span class="pill-icon"></span>
<span class="pill-label" data-i18n="tasks-paused">Paused</span>
<span class="pill-count" id="count-paused">-</span>
</button>
<button
class="filter-pill"
data-filter="blocked"
hx-get="/api/ui/tasks?filter=blocked"
hx-target="closest .window-body #task-list"
hx-swap="innerHTML"
>
<span class="pill-icon"></span>
<span class="pill-label" data-i18n="tasks-blocked"
>Blocked/Issues</span
>
<span class="pill-count" id="count-blocked">-</span>
</button>
<div class="time-saved-badge">
<span class="time-label" data-i18n="tasks-time-saved"
>Active Time Saved:</span
>
<span class="time-value" id="time-saved-value">-</span>
</div>
</div>
<!-- Quick Intent Input -->
<div class="quick-intent-bar">
<div class="intent-input-wrapper">
<input
type="text"
id="quick-intent-input"
name="intent"
class="quick-intent-input"
placeholder="What would you like to do? e.g., 'create a CRM app' or 'remind me to call John tomorrow'"
data-i18n-placeholder="tasks-input-placeholder"
autocomplete="off"
/>
<button
id="quick-intent-btn"
class="btn-create-run"
hx-post="/api/ui/autotask/create"
hx-ext="json-enc"
hx-include="#quick-intent-input"
hx-target="closest .window-body #intent-result-hidden"
hx-swap="none"
hx-indicator="#intent-spinner"
hx-timeout="300000"
>
<span class="btn-text">Create & Run</span>
<span class="spinner" id="intent-spinner"></span>
</button>
</div>
<div id="intent-result" class="intent-result"></div>
<div id="intent-result-hidden" style="display: none"></div>
</div>
<!-- Main Two-Column Layout with Splitter -->
<main class="tasks-main">
<!-- Left Panel: Task Cards List -->
<section class="tasks-list-panel">
<div
class="tasks-list-scroll"
id="task-list"
hx-get="/api/ui/tasks?filter=all"
hx-trigger="load, taskCreated from:div throttle:2s"
hx-swap="innerHTML transition:false"
>
<!-- Loading state - replaced by HTMX -->
<div class="loading-state">
<div class="loading-spinner"></div>
<p>Loading tasks...</p>
</div>
</div>
</section>
<!-- Splitter -->
<div class="tasks-splitter" id="tasks-splitter"></div>
<!-- Right Panel: Task Detail -->
<aside class="task-detail-panel" id="task-detail-panel">
<!-- Detail content loaded dynamically -->
<div class="detail-empty" id="detail-empty">
<div class="empty-icon">📋</div>
<h3 class="empty-title">Select a task</h3>
<p class="empty-description">
Click on a task from the list to view details
</p>
<div class="empty-info">
<p>
<strong>Bot Database:</strong> All apps share the same
database tables.
</p>
<p>
<strong>Shared Resources:</strong> Schedulers, tools,
and monitors work across all apps.
</p>
</div>
</div>
<!-- Dynamic detail content -->
<div
id="task-detail-content"
style="display: none"
hx-get=""
hx-trigger="taskSelected from:div"
hx-swap="innerHTML"
>
<!-- Loaded via HTMX when task selected -->
</div>
</aside>
</main>
</div>
<!-- Floating Progress Panel - Shows live task generation progress -->
<div
class="floating-progress-panel"
id="floating-progress"
style="display: none"
>
<div class="floating-progress-header">
<div class="floating-progress-title">
<span class="progress-dot"></span>
<span id="floating-task-name">Processing...</span>
</div>
<div class="floating-progress-actions">
<button class="btn-minimize" onclick="minimizeFloatingProgress()">
</button>
<button class="btn-close-float" onclick="closeFloatingProgress()">
×
</button>
</div>
</div>
<div class="floating-progress-div">
<div class="floating-progress-bar">
<div
class="floating-progress-fill"
id="floating-progress-fill"
style="width: 0%"
></div>
</div>
<div class="floating-progress-info">
<span id="floating-progress-step">Starting...</span>
<span id="floating-progress-percent">0%</span>
</div>
<div class="floating-progress-log" id="floating-progress-log">
<!-- Live log entries appear here -->
</div>
<div class="floating-llm-terminal" id="floating-llm-terminal">
<!-- LLM streaming output appears here -->
</div>
</div>
</div>
<!-- Toast Container -->
<div class="toast-container" id="toast-container"></div>
<!-- New Intent Modal -->
<div class="modal" id="new-intent-modal" style="display: none">
<div class="modal-backdrop" onclick="closeNewIntentModal()"></div>
<div class="modal-content">
<div class="modal-header">
<h3>Create New Intent</h3>
<button class="btn-close" onclick="closeNewIntentModal()">×</button>
</div>
<div class="modal-div">
<form id="new-intent-form">
<div class="form-group">
<label for="intent-text"
>What would you like to accomplish?</label
>
<textarea
id="intent-text"
name="intent"
rows="4"
placeholder="Describe what you want to create or automate..."
></textarea>
</div>
<div class="form-row">
<div class="form-group">
<label for="intent-priority">Priority</label>
<select id="intent-priority" name="priority">
<option value="low">Low</option>
<option value="medium" selected>Medium</option>
<option value="high">High</option>
<option value="urgent">Urgent</option>
</select>
</div>
<div class="form-group">
<label for="intent-mode">Execution Mode</label>
<select id="intent-mode" name="mode">
<option value="auto">Automatic</option>
<option value="supervised">Supervised</option>
<option value="manual">Manual Approval</option>
</select>
</div>
</div>
</form>
</div>
<div class="modal-footer">
<button class="btn-secondary" onclick="closeNewIntentModal()">
Cancel
</button>
<button class="btn-primary" onclick="submitNewIntent()">
Create & Run
</button>
</div>
</div>
</div>
<!-- Decision Modal -->
<div class="modal" id="decision-modal" style="display: none">
<div class="modal-backdrop" onclick="closeDecisionModal()"></div>
<div class="modal-content modal-lg">
<div class="modal-header">
<h3>Make Decision</h3>
<button class="btn-close" onclick="closeDecisionModal()">×</button>
</div>
<div class="modal-div">
<div class="decision-question" id="decision-question">
<h4>Decision Required</h4>
<p>Loading decision details...</p>
</div>
<div class="decision-options" id="decision-options">
<!-- Options loaded dynamically -->
</div>
</div>
<div class="modal-footer">
<button class="btn-secondary" onclick="closeDecisionModal()">
Cancel
</button>
<button class="btn-secondary" onclick="skipDecision()">
Skip for Now
</button>
<button class="btn-primary" onclick="submitDecision()">
Confirm Decision
</button>
</div>
</div>
</div>
<link rel="stylesheet" href="/suite/tasks/tasks-scoped.css" />
<script src="/suite/tasks/tasks-scoped.js"></script>

874
ui/suite/partials/vibe.html Normal file
View file

@ -0,0 +1,874 @@
<!-- Vibe Window — APP_CREATE Canvas + Agents IDE (Phase 7) -->
<link rel="stylesheet" href="/suite/vibe/agents-sidebar.css" />
<div class="vibe-container" id="vibeWindow">
<!-- Pipeline Tabs -->
<div class="vibe-pipeline">
<button class="vibe-pipeline-tab" data-stage="plan">// PLAN</button>
<button class="vibe-pipeline-tab active" data-stage="build">// BUILD</button>
<button class="vibe-pipeline-tab" data-stage="review">// REVIEW</button>
<button class="vibe-pipeline-tab" data-stage="deploy">// DEPLOY</button>
<button class="vibe-pipeline-tab" data-stage="monitor">// MONITOR</button>
</div>
<!-- Main Content: Canvas + Sidebar -->
<div class="vibe-body">
<!-- Agents & Workspaces Sidebar (Fixed to left as in image) -->
<aside class="agents-sidebar" id="agentsSidebar" style="background: var(--surface);">
<div class="as-logo-section"
style="padding: 16px; border-bottom: 1px solid var(--border); display: flex; align-items: center; justify-content: space-between;">
<h2
style="margin:0; font-size: 15px; color: var(--text); font-weight: 800; display: flex; align-items: center; gap: 8px;">
<span style="font-size: 18px; color: var(--accent);">🌱</span> mantis farm
</h2>
<div style="display:flex; gap: 12px; color: var(--text-muted); font-size: 14px;">
<span style="cursor:pointer;" title="Home"></span>
<span style="cursor:pointer;" title="Overview"></span>
</div>
</div>
<div class="as-section">
<div class="as-section-header">
<h3>Agents</h3>
<button class="as-collapse-btn" id="agentsSidebarCollapse" type="button"></button>
</div>
<div class="as-agent-list" id="asAgentList">
<!-- Mantis #1 EVOLVED -->
<div class="as-agent-card" data-agent-id="1" style="border-left: 3px solid var(--accent);">
<div class="as-agent-header">
<span class="as-status-dot green"></span>
<span class="as-agent-name">Mantis #1</span>
<span class="as-drag-handle" style="margin-left:auto;"></span>
</div>
<div class="as-agent-body">
<span class="as-agent-icons">👀 ⚙️ ⚡</span>
<span class="as-badge badge-evolved">EVOLVED</span>
</div>
</div>
<!-- Mantis #2 BRED -->
<div class="as-agent-card" data-agent-id="2">
<div class="as-agent-header">
<span class="as-status-dot yellow"></span>
<span class="as-agent-name">Mantis #2</span>
<span class="as-drag-handle" style="margin-left:auto;"></span>
</div>
<div class="as-agent-body">
<span class="as-agent-icons">🥚</span>
<span class="as-badge badge-bred">BRED</span>
</div>
</div>
<!-- Mantis #3 & #4 WILD -->
<div class="as-agent-card" style="opacity: 0.6;" data-agent-id="3">
<div class="as-agent-header">
<span class="as-status-dot gray"></span>
<span class="as-agent-name">Mantis #3</span>
<span class="as-drag-handle" style="margin-left:auto;"></span>
</div>
<div class="as-agent-body">
<span class="as-agent-icons" style="filter: grayscale(1);">🥚</span>
<span class="as-badge badge-wild" style="background: var(--surface-active, #ccc)">WILD</span>
</div>
</div>
<div class="as-agent-card" style="opacity: 0.6;" data-agent-id="4">
<div class="as-agent-header">
<span class="as-status-dot gray"></span>
<span class="as-agent-name">Mantis #4</span>
<span class="as-drag-handle" style="margin-left:auto;"></span>
</div>
<div class="as-agent-body">
<span class="as-agent-icons" style="filter: grayscale(1);">🥚</span>
<span class="as-badge badge-wild" style="background: var(--surface-active, #ccc)">WILD</span>
</div>
</div>
</div>
<div style="padding: 0 8px;">
<button class="as-create-btn" id="createAgentBtn" type="button"
style="width: calc(100% - 16px); margin: 8px;">+ Create a New Mantis</button>
</div>
</div>
<div class="as-section">
<div class="as-section-header">
<h3>Workspaces</h3>
</div>
<div class="as-workspace-list" id="asWorkspaceList">
<div class="as-workspace-item">
<button class="as-workspace-toggle" type="button"
style="background: var(--bg); border-left: 3px solid var(--accent);">
<span class="as-workspace-arrow"></span>
<span>E-Commerce App Development</span>
</button>
<div class="as-workspace-body" style="display:block;">
<div class="as-workspace-agent">Mantis #1</div>
<div class="as-workspace-agent">Mantis #4</div>
<div class="as-workspace-dropzone" data-workspace="ecommerce">
Drag a Mantis to Include
</div>
</div>
</div>
<div class="as-workspace-item">
<button class="as-workspace-toggle" type="button">
<span class="as-workspace-arrow"></span>
<span>Accountability App Development</span>
</button>
<div class="as-workspace-body" style="display:none;">
<div class="as-workspace-agent">Mantis #4</div>
<div class="as-workspace-dropzone" data-workspace="accountability">
Drag a Mantis to Include
</div>
</div>
</div>
</div>
<div style="padding: 0 8px;">
<button class="as-create-btn" id="createWorkspaceBtn" type="button"
style="width: calc(100% - 16px); margin: 8px;">+ Create a New Project</button>
</div>
</div>
</aside>
<!-- Canvas Area -->
<div class="vibe-canvas" id="vibeCanvas"
style="background: var(--bg, #fdfdfd); background-image: radial-gradient(var(--border) 1px, transparent 1px); background-size: 20px 20px; position:relative;">
<div
style="padding: 16px 24px; font-size: 11px; color: var(--text-muted); font-weight: 600; letter-spacing: 0.5px; border-bottom: 1px solid var(--border); background: rgba(255,255,255,0.8); backdrop-filter: blur(4px);">
// DASHBOARD <span style="color: var(--text-secondary); margin:0 6px;">&gt;</span> // E-COMMERCE APP DEVELOPMENT
<div style="float: right;">
<button
style="border: 1px solid var(--border); background: var(--bg); border-radius: 4px; padding: 2px 8px; cursor:pointer;">-</button>
<span style="font-size: 11px; margin: 0 8px; color: var(--text);">100%</span>
<button
style="border: 1px solid var(--border); background: var(--bg); border-radius: 4px; padding: 2px 8px; cursor:pointer;">+</button>
</div>
</div>
<!-- Steps Nodes (populated dynamically from chat/API) -->
<div class="vibe-steps" id="vibeSteps"
style="padding: 40px; display: none; gap: 60px; align-items: flex-start; overflow-x: auto;">
</div>
<!-- Empty state -->
<div id="vibeCanvasEmpty"
style="flex:1; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 16px; padding: 60px; text-align: center;">
<div style="font-size: 56px; animation: float 3s ease-in-out infinite;">🌱</div>
<h3
style="margin: 0; font-size: 22px; font-weight: 800; color: var(--text); font-family: 'Fira Code', monospace;">
Vibe — App Builder</h3>
<p style="margin: 0; font-size: 14px; color: var(--text-muted); max-width: 440px; line-height: 1.6;">
Describe what you want to build in the chat. Mantis #1 will analyze your request, generate task
nodes on this canvas, and build the entire application for you.
</p>
<div style="display: flex; gap: 10px; flex-wrap: wrap; justify-content: center; margin-top: 12px;">
<button class="vibe-quick-btn" type="button"
onclick="document.getElementById('vibeChatInput').value='Create an e-commerce app for selling handmade crafts with shopping cart and payments'; document.getElementById('vibeChatInput').focus();"
style="padding: 8px 16px; border: 1px solid var(--border); border-radius: 20px; background: var(--bg); font-size: 12px; cursor: pointer; color: var(--text-muted); transition: all 0.15s; font-family: 'Fira Code', monospace;">
🛍️ E-Commerce App
</button>
<button class="vibe-quick-btn" type="button"
onclick="document.getElementById('vibeChatInput').value='Build a CRM system with contacts, leads, and deal pipeline tracking'; document.getElementById('vibeChatInput').focus();"
style="padding: 8px 16px; border: 1px solid var(--border); border-radius: 20px; background: var(--bg); font-size: 12px; cursor: pointer; color: var(--text-muted); transition: all 0.15s; font-family: 'Fira Code', monospace;">
📇 CRM System
</button>
<button class="vibe-quick-btn" type="button"
onclick="document.getElementById('vibeChatInput').value='Create a project management dashboard with tasks, Kanban board, and team assignments'; document.getElementById('vibeChatInput').focus();"
style="padding: 8px 16px; border: 1px solid var(--border); border-radius: 20px; background: var(--bg); font-size: 12px; cursor: pointer; color: var(--text-muted); transition: all 0.15s; font-family: 'Fira Code', monospace;">
📊 Project Manager
</button>
</div>
</div>
<!-- Vibe Chat Overlay (LIVE) -->
<div id="vibeChatOverlay"
style="position: absolute; bottom: 24px; right: 24px; width: 380px; background: var(--surface); border-radius: 12px; box-shadow: 0 8px 24px rgba(0,0,0,0.2); display: flex; flex-direction: column; overflow: hidden; border: 1px solid var(--border); color: var(--bg); z-index: 100;">
<div
style="padding: 12px 16px; background: var(--surface-hover); border-bottom: 1px solid var(--border); display: flex; justify-content: space-between; align-items: center;">
<div style="display: flex; align-items: center; gap: 8px;">
<span class="as-status-dot green" id="vibeChatStatusDot"
style="box-shadow: 0 0 8px var(--accent);"></span>
<span style="font-size: 12px; font-weight: 600;">Mantis #1</span>
</div>
<span id="vibeChatStatusBadge"
style="font-size: 10px; color: var(--text-muted); background: #333; padding: 2px 6px; border-radius: 4px;">CONNECTING…</span>
</div>
<div id="vibeChatMessages"
style="padding: 16px; display: flex; flex-direction: column; gap: 12px; font-size: 12px; line-height: 1.5; min-height: 220px; max-height: 350px; overflow-y: auto; font-family: 'Segoe UI', system-ui, sans-serif;">
<!-- Welcome hint -->
<div
style="align-self: center; background: rgba(132,214,105,0.12); color: var(--accent); padding: 8px 14px; border-radius: 8px; font-size: 11px; text-align: center;">
💡 TIP: Describe your project. The more detail, the better the plan.
</div>
</div>
<div style="padding: 12px; border-top: 1px solid var(--border); background: var(--surface);">
<form id="vibeChatForm" autocomplete="off"
style="display: flex; align-items: center; gap: 8px; background: var(--surface-hover); padding: 8px 12px; border-radius: 20px; border: 1px solid #333;">
<span style="color: var(--text-muted); cursor: pointer; transform: rotate(-45deg);">📎</span>
<input type="text" id="vibeChatInput" placeholder="Describe your project…"
style="flex: 1; background: transparent; border: none; color: var(--bg); font-size: 13px; outline: none; font-family: 'Segoe UI', system-ui, sans-serif;" />
<button type="submit" id="vibeChatSend"
style="background: var(--accent); color: var(--surface); width: 24px; height: 24px; border-radius: 50%; display: flex; align-items: center; justify-content: center; cursor: pointer; font-size: 14px; font-weight: bold; border: none;">↑</button>
</form>
</div>
</div>
<!-- Preview Panel (hidden by default) -->
<div class="vibe-preview" id="vibePreview" style="display:none;">
<div class="vibe-preview-header">
<span>// PREVIEW</span>
<input type="text" class="vibe-preview-url" id="vibePreviewUrl" value="" readonly />
</div>
<div class="vibe-preview-content" id="vibePreviewContent"></div>
</div>
</div>
</div>
</div>
<script>
(function () {
"use strict";
/* ── state ── */
var vibeWs = null;
var vibeSessionId = null;
var vibeUserId = null;
var vibeBotId = "default";
var vibeBotName = "default";
var vibeStreaming = false;
var vibeStreamId = null;
var vibeStreamContent = "";
var taskNodes = [];
var currentProject = "My App";
var nodeIdCounter = 0;
/* ── helpers ── */
function esc(text) {
var d = document.createElement("div");
d.textContent = text || "";
return d.innerHTML;
}
function vibeAddMsg(role, text) {
var box = document.getElementById("vibeChatMessages");
if (!box) return;
var div = document.createElement("div");
if (role === "user") {
div.style.cssText = "align-self:flex-end;background:var(--accent);color:var(--surface);font-weight:500;padding:10px 14px;border-radius:12px 12px 0 12px;max-width:85%;word-wrap:break-word;";
div.textContent = text;
} else if (role === "system") {
div.style.cssText = "align-self:center;background:rgba(132,214,105,0.12);color: var(--accent);padding:6px 12px;border-radius:8px;font-size:11px;text-align:center;";
div.innerHTML = text;
} else {
div.style.cssText = "align-self:flex-start;background: var(--border);color:#ececec;padding:10px 14px;border-radius:12px 12px 12px 0;max-width:85%;word-wrap:break-word;";
div.className = "vibe-bot-msg";
if (typeof marked !== "undefined" && marked.parse) {
div.innerHTML = marked.parse(text);
} else {
div.textContent = text;
}
}
box.appendChild(div);
box.scrollTop = box.scrollHeight;
return div;
}
function vibeAddStreamStart() {
vibeStreamId = "vibe-stream-" + Date.now();
vibeStreamContent = "";
var el = vibeAddMsg("bot", "▍");
if (el) el.id = vibeStreamId;
return el;
}
function vibeUpdateStream(content) {
vibeStreamContent += (content || "");
var el = document.getElementById(vibeStreamId);
if (!el) return;
if (typeof marked !== "undefined" && marked.parse) {
el.innerHTML = marked.parse(vibeStreamContent);
} else {
el.textContent = vibeStreamContent;
}
var box = document.getElementById("vibeChatMessages");
if (box) box.scrollTop = box.scrollHeight;
}
function vibeFinalizeStream() {
var el = document.getElementById(vibeStreamId);
if (el) {
if (typeof marked !== "undefined" && marked.parse) {
el.innerHTML = marked.parse(vibeStreamContent);
} else {
el.textContent = vibeStreamContent;
}
el.removeAttribute("id");
}
vibeStreamId = null;
vibeStreamContent = "";
vibeStreaming = false;
}
/* ── update status badge ── */
function setVibeStatus(status) {
var dot = document.getElementById("vibeChatStatusDot");
var badge = document.getElementById("vibeChatStatusBadge");
if (status === "connected") {
if (dot) { dot.className = "as-status-dot green"; dot.style.boxShadow = "0 0 8px var(--accent)"; }
if (badge) { badge.textContent = "EVOLVED"; badge.style.background = "var(--accent)"; badge.style.color = "var(--bg)"; }
} else if (status === "connecting") {
if (dot) { dot.className = "as-status-dot yellow"; dot.style.boxShadow = "0 0 8px #f59e0b"; }
if (badge) { badge.textContent = "CONNECTING…"; badge.style.background = "#333"; badge.style.color = "var(--text-muted)"; }
} else {
if (dot) { dot.className = "as-status-dot red"; dot.style.boxShadow = "0 0 8px #ef4444"; }
if (badge) { badge.textContent = "OFFLINE"; badge.style.background = "#333"; badge.style.color = "var(--text-muted)"; }
}
}
/* ── update agent sidebar card ── */
function updateMantis1(status, detail) {
var card = document.querySelector('.as-agent-card[data-agent-id="1"]');
if (!card) return;
var bar = card.querySelector(".as-agent-bar .as-bar-fill");
if (status === "working") {
card.style.borderLeftColor = "#f59e0b";
if (!card.querySelector(".as-agent-bar")) {
var barWrapper = document.createElement("div");
barWrapper.className = "as-agent-bar";
barWrapper.innerHTML = '<div class="as-bar-fill bred" style="width:0%;transition:width 0.5s;"></div>';
card.appendChild(barWrapper);
}
} else if (status === "done") {
card.style.borderLeftColor = "var(--accent)";
bar = card.querySelector(".as-bar-fill");
if (bar) bar.style.width = "100%";
setTimeout(function () {
var b = card.querySelector(".as-agent-bar");
if (b) b.remove();
}, 2000);
}
}
/* ── create task node on canvas ── */
function addTaskNode(title, description, meta) {
var stepsContainer = document.getElementById("vibeSteps");
if (!stepsContainer) return;
stepsContainer.style.display = "flex";
var emptyState = document.getElementById("vibeCanvasEmpty");
if (emptyState) emptyState.style.display = "none";
nodeIdCounter++;
meta = meta || {};
var fileCount = meta.estimated_files || meta.files || Math.floor(Math.random() * 15 + 3);
var time = meta.estimated_time || meta.time || Math.floor(Math.random() * 20 + 5) + "m";
var tokens = meta.estimated_tokens || meta.tokens || "~" + Math.floor(Math.random() * 30 + 10) + "k tokens";
var status = meta.status || "Planning";
var fileList = meta.fileList || [];
var isFirst = stepsContainer.children.length === 0;
var nodeId = "vibe-node-" + nodeIdCounter;
var statusBg = status === "Done" ? "var(--accent)" : (status === "Planning" ? "var(--success-light, #eef8eb)" : "var(--warning-light, var(--bg)3cd)");
var statusColor = status === "Done" ? "var(--bg)" : (status === "Planning" ? "var(--accent)" : "var(--warning, #856404)");
var subTasksHtml = "";
if (fileList.length > 0) {
subTasksHtml = '<div id="' + nodeId + '-files" style="display:none;padding:8px 16px;border-top:1px solid var(--border);font-size:10px;color:var(--text-muted, #555);">';
for (var fi = 0; fi < fileList.length; fi++) {
subTasksHtml += '<div style="padding:2px 0;display:flex;align-items:center;gap:4px;"><span style="color: var(--accent);">📄</span> ' + esc(fileList[fi]) + '</div>';
}
subTasksHtml += '</div>';
}
var node = document.createElement("div");
node.className = "vibe-task-node";
node.style.cssText = "background: var(--bg);border:" + (isFirst ? "2px solid var(--accent)" : "1px solid var(--border)") + ";border-radius:8px;width:280px;box-shadow:0 " + (isFirst ? "4" : "2") + "px 12px rgba(" + (isFirst ? "132,214,105,0.15" : "0,0,0,0.05") + ");position:relative;flex-shrink:0;animation:nodeIn 0.4s ease;";
node.innerHTML =
'<div style="padding:12px 16px;border-bottom: 1px solid var(--border);">' +
'<div style="display:flex;justify-content:space-between;margin-bottom:8px;font-size:10px;color: var(--text-muted);">' +
'<span>' + fileCount + ' files</span><span>' + time + '</span><span>' + tokens + '</span>' +
'</div>' +
'<h4 style="margin:0 0 8px 0;font-size:14px;color: var(--text);font-weight:700;">' + esc(title) + '</h4>' +
'<p style="margin:0;font-size:11px;color: var(--text-muted);line-height:1.4;">' + esc(description) + '</p>' +
'</div>' +
'<div style="padding:10px 16px;background: var(--surface);border-bottom: 1px solid var(--border);font-size:11px;">' +
'<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:8px;">' +
'<span style="color: var(--text-muted);">Status</span>' +
'<span style="background:' + statusBg + ';color:' + statusColor + ';padding:2px 8px;border-radius:12px;font-weight:600;">' + esc(status) + '</span>' +
'</div>' +
'<div style="display:flex;justify-content:space-between;align-items:center;">' +
'<span style="color: var(--text-muted);">Mantis Manager</span>' +
'<span style="display:flex;align-items:center;gap:4px;"><span class="as-status-dot green"></span> Mantis #1</span>' +
'</div>' +
'</div>' +
'<div style="padding:8px 16px;font-size:10px;font-weight:700;color: var(--text-muted);">' +
'<div data-toggle="' + nodeId + '-files" style="padding:4px 0;cursor:pointer;user-select:none;" onclick="(function(el){var t=document.getElementById(el.getAttribute(\'data-toggle\'));if(t){t.style.display=t.style.display===\'none\'?\'\':\'none\';var a=el.querySelector(\'span\');if(a)a.textContent=t.style.display===\'none\'?\'▶\':\'▼\';}})(this)">// SUB-TASKS <span style="float:right;"></span></div>' +
'<div style="padding:4px 0;cursor:pointer;">// LOGS <span style="float:right;"></span></div>' +
'</div>' +
subTasksHtml;
if (isFirst || stepsContainer.children.length > 0) {
var line = document.createElement("div");
line.style.cssText = "position:absolute;right:-60px;top:50%;width:60px;height:2px;background:var(--accent);z-index:10;";
node.appendChild(line);
if (!isFirst) {
var dot = document.createElement("div");
dot.style.cssText = "position:absolute;left:-5px;top:50%;transform:translateY(-50%);width:10px;height:10px;border-radius:50%;background:var(--accent);z-index:20;";
node.appendChild(dot);
}
}
stepsContainer.appendChild(node);
stepsContainer.scrollLeft = stepsContainer.scrollWidth;
taskNodes.push({ title: title, description: description, meta: meta });
return node;
}
/* ── call /api/autotask/classify (real backend) ── */
function callAutotask(intent) {
updateMantis1("working");
vibeAddMsg("system", "🔄 Mantis #1 is analyzing your request…");
// Connect task progress WS to get live orchestrator events
connectTaskProgressWs(null);
// Update breadcrumb
var breadcrumb = document.querySelector(".vibe-canvas div:first-child");
if (breadcrumb) {
currentProject = intent.substring(0, 40).replace(/[^a-zA-Z0-9 ]/g, "");
breadcrumb.innerHTML = '// DASHBOARD <span style="color: var(--text-secondary);margin:0 6px;">&gt;</span> // ' + esc(currentProject.toUpperCase()) + ' <div style="float:right;"><button style="border: 1px solid var(--border);background: var(--bg);border-radius:4px;padding:2px 8px;cursor:pointer;">-</button><span style="font-size:11px;margin:0 8px;color: var(--text);">100%</span><button style="border: 1px solid var(--border);background: var(--bg);border-radius:4px;padding:2px 8px;cursor:pointer;">+</button></div>';
}
fetch("/api/autotask/classify", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ intent: intent, auto_process: true })
})
.then(function (r) { return r.json(); })
.then(function (data) {
updateMantis1("done");
if (data.success && data.result) {
var r = data.result;
// Connect to task-specific progress WS if we have a task_id
if (r.task_id) {
connectTaskProgressWs(r.task_id);
}
// Add task nodes from created_resources
if (r.created_resources && r.created_resources.length > 0) {
r.created_resources.forEach(function (res, i) {
setTimeout(function () {
addTaskNode(
res.name || res.resource_type,
res.resource_type + (res.path ? " → " + res.path : ""),
{ status: "Done" }
);
}, i * 400);
});
} else {
addTaskNode("Project Setup", "Setting up: " + intent, { status: "Planning" });
}
vibeAddMsg("bot", r.message || "Done! Your project is ready.");
if (r.app_url) {
vibeAddMsg("system", '✅ App available at <a href="' + r.app_url + '" target="_blank" style="color: var(--accent);text-decoration:underline;">' + esc(r.app_url) + '</a>');
var preview = document.getElementById("vibePreview");
var urlBar = document.getElementById("vibePreviewUrl");
var content = document.getElementById("vibePreviewContent");
if (preview) preview.style.display = "";
if (urlBar) urlBar.value = r.app_url;
if (content) content.innerHTML = '<iframe src="' + r.app_url + '" style="width:100%;height:100%;border:none;"></iframe>';
}
if (r.next_steps && r.next_steps.length > 0) {
vibeAddMsg("bot", "**Next steps:**\n" + r.next_steps.map(function (s) { return "• " + s; }).join("\n"));
}
} else {
vibeAddMsg("bot", "I classified your intent as **" + (data.intent_type || "UNKNOWN") + "**. " + (data.error || "Processing complete."));
addTaskNode("Analysis", intent, { status: "Planning" });
}
})
.catch(function (err) {
updateMantis1("done");
vibeAddMsg("system", "⚠️ Backend unavailable — showing plan preview.");
var words = intent.split(/[.,;]/);
addTaskNode("Project Setup", "Create project structure and install dependencies", { status: "Planning" });
if (words.length > 1) {
setTimeout(function () {
addTaskNode("Database Schema", "Define tables for: " + words.slice(0, 3).join(", "), { status: "Pending" });
}, 500);
}
vibeAddMsg("bot", "I've created a preliminary plan with " + Math.min(words.length + 1, 5) + " nodes. Once the backend is available, I'll process the full build.");
});
}
/* ── WebSocket to backend (re-use chat WS) ── */
function connectVibeWs() {
setVibeStatus("connecting");
var botName = window.__INITIAL_BOT_NAME__ || "default";
fetch("/api/auth?bot_name=" + encodeURIComponent(botName))
.then(function (r) { return r.json(); })
.then(function (auth) {
vibeUserId = auth.user_id;
vibeSessionId = auth.session_id;
vibeBotId = auth.bot_id || "default";
vibeBotName = botName;
var proto = location.protocol === "https:" ? "wss://" : "ws://";
var url = proto + location.host + "/ws?session_id=" + vibeSessionId + "&user_id=" + vibeUserId + "&bot_name=" + vibeBotName;
vibeWs = new WebSocket(url);
vibeWs.onopen = function () {
setVibeStatus("connected");
};
vibeWs.onmessage = function (event) {
try {
var data = JSON.parse(event.data);
if (data.type === "connected") return;
if (data.event) return; // system events
// Agent-mode messages → update sidebar
if (data.type === "thought_process") {
vibeAddMsg("system", "💭 " + esc(data.content));
return;
}
if (data.type === "terminal_output") {
vibeAddMsg("system", "🖥️ " + esc(data.line));
return;
}
if (data.type === "step_progress") {
var pct = Math.round((data.current / data.total) * 100);
updateMantis1("working");
var bar = document.querySelector('.as-agent-card[data-agent-id="1"] .as-bar-fill');
if (bar) bar.style.width = pct + "%";
return;
}
// Bot responses → chat
if (data.message_type === 2) {
if (data.is_complete) {
if (vibeStreaming) {
vibeFinalizeStream();
} else if (data.content && data.content.trim()) {
vibeAddMsg("bot", data.content);
}
vibeStreaming = false;
} else {
if (!vibeStreaming) {
vibeStreaming = true;
vibeAddStreamStart();
vibeUpdateStream(data.content || "");
} else {
vibeUpdateStream(data.content || "");
}
}
}
} catch (e) {
console.error("Vibe WS parse error:", e);
}
};
vibeWs.onclose = function () { setVibeStatus("disconnected"); };
vibeWs.onerror = function () { setVibeStatus("disconnected"); };
})
.catch(function () {
setVibeStatus("disconnected");
vibeAddMsg("system", "⚠️ Could not connect to backend. You can still plan offline.");
});
}
/* ── send via WS (for regular chat) ── */
function vibeSendWs(content) {
if (vibeWs && vibeWs.readyState === WebSocket.OPEN) {
vibeWs.send(JSON.stringify({
bot_id: vibeBotId,
user_id: vibeUserId,
session_id: vibeSessionId,
channel: "web",
content: content,
message_type: 1,
timestamp: new Date().toISOString()
}));
}
}
/* ── Task Progress WebSocket (orchestrator events) ── */
var taskProgressWs = null;
function connectTaskProgressWs(taskId) {
var proto = location.protocol === "https:" ? "wss://" : "ws://";
var url = proto + location.host + "/ws/task-progress" + (taskId ? "/" + taskId : "");
if (taskProgressWs) {
try { taskProgressWs.close(); } catch (ignore) { }
}
taskProgressWs = new WebSocket(url);
taskProgressWs.onmessage = function (event) {
try {
var data = JSON.parse(event.data);
if (data.type === "connected") return;
if (data.event_type === "agent_thought" || data.step === "agent_thought") {
var agentLabel = (data.details || "mantis_1").replace("mantis_", "Mantis #");
vibeAddMsg("system", "💭 " + agentLabel + ": " + esc(data.text || data.message || ""));
return;
}
if (data.event_type === "agent_update" || data.step === "agent_update") {
try {
var info = typeof data.details === "string" ? JSON.parse(data.details) : data.details;
if (info) {
updateAgentCard(info.agent_id, info.status, info.detail);
}
} catch (ignore) { }
return;
}
if (data.event_type === "task_node" || data.step === "task_node") {
try {
var nodeInfo = typeof data.details === "string" ? JSON.parse(data.details) : data.details;
if (nodeInfo) {
addTaskNode(
nodeInfo.title || data.message || "Task",
nodeInfo.description || "",
{
status: nodeInfo.status || "Planning",
estimated_files: nodeInfo.estimated_files,
estimated_time: nodeInfo.estimated_time,
estimated_tokens: nodeInfo.estimated_tokens,
fileList: nodeInfo.files || []
}
);
}
} catch (ignore) {
addTaskNode(data.message || "Task", "", { status: "Planning" });
}
return;
}
if (data.event_type === "step_progress" || data.step === "step_progress") {
var pct = 0;
if (data.current_step && data.total_steps) {
pct = Math.round((data.current_step / data.total_steps) * 100);
} else if (data.current && data.total) {
pct = Math.round((data.current / data.total) * 100);
}
updateMantis1("working");
var bar = document.querySelector('.as-agent-card[data-agent-id="1"] .as-bar-fill');
if (bar) bar.style.width = pct + "%";
var stageMap = {
"Planning": "plan", "Building": "build",
"Reviewing": "review", "Deploying": "deploy",
"Monitoring": "monitor"
};
var stageLabel = data.message || "";
var tabStage = stageMap[stageLabel];
if (tabStage) {
var allTabs = document.querySelectorAll(".vibe-pipeline-tab");
allTabs.forEach(function (t) { t.classList.remove("active"); });
var activeTab = document.querySelector('.vibe-pipeline-tab[data-stage="' + tabStage + '"]');
if (activeTab) activeTab.classList.add("active");
}
return;
}
if (data.event_type === "pipeline_complete" || data.step === "pipeline_complete") {
updateMantis1("done");
vibeAddMsg("system", "✅ Pipeline complete — all stages finished");
return;
}
if (data.event_type === "manifest_update") {
return;
}
} catch (e) {
console.error("Task progress parse error:", e);
}
};
taskProgressWs.onerror = function () { };
taskProgressWs.onclose = function () { };
}
function updateAgentCard(agentId, status, detail) {
var card = document.querySelector('.as-agent-card[data-agent-id="' + agentId + '"]');
if (!card) return;
card.style.opacity = "1";
var badge = card.querySelector(".as-badge");
var dot = card.querySelector(".as-status-dot");
if (status === "WORKING") {
card.style.borderLeft = "3px solid #f59e0b";
if (dot) { dot.className = "as-status-dot yellow"; }
if (badge) { badge.textContent = "WORKING"; badge.className = "as-badge badge-bred"; }
if (!card.querySelector(".as-agent-bar")) {
var barWrapper = document.createElement("div");
barWrapper.className = "as-agent-bar";
barWrapper.innerHTML = '<div class="as-bar-fill bred" style="width:0%;transition:width 0.5s;"></div>';
card.appendChild(barWrapper);
}
} else if (status === "EVOLVED" || status === "DONE") {
card.style.borderLeft = "3px solid var(--accent)";
if (dot) { dot.className = "as-status-dot green"; }
if (badge) { badge.textContent = "EVOLVED"; badge.className = "as-badge badge-evolved"; }
var agBar = card.querySelector(".as-bar-fill");
if (agBar) agBar.style.width = "100%";
setTimeout(function () {
var b = card.querySelector(".as-agent-bar");
if (b) b.remove();
}, 2000);
} else if (status === "BRED") {
card.style.borderLeft = "3px solid #f59e0b";
if (dot) { dot.className = "as-status-dot yellow"; }
if (badge) { badge.textContent = "BRED"; badge.className = "as-badge badge-bred"; }
} else if (status === "FAILED") {
card.style.borderLeft = "3px solid #ef4444";
if (dot) { dot.className = "as-status-dot red"; }
if (badge) { badge.textContent = "FAILED"; badge.className = "as-badge badge-bred"; badge.style.background = "#ef4444"; }
}
if (detail) {
var detailEl = card.querySelector(".as-agent-detail");
if (!detailEl) {
detailEl = document.createElement("span");
detailEl.className = "as-agent-detail";
detailEl.style.cssText = "font-size:10px;color: var(--text-muted);display:block;padding:0 12px 4px;";
var body = card.querySelector(".as-agent-body");
if (body) body.after(detailEl);
}
detailEl.textContent = detail;
}
}
/* ── form submit ── */
function handleVibeSubmit(e) {
e.preventDefault();
var input = document.getElementById("vibeChatInput");
if (!input) return;
var text = input.value.trim();
if (!text) return;
input.value = "";
vibeAddMsg("user", text);
// Decide: if the text looks like a "build" request → call autotask API
// Otherwise → send via WS for regular chat
var buildKeywords = /\b(create|build|make|develop|generate|design|scaffold|i want|i need|app for|website for|system for|platform for|dashboard for)\b/i;
if (buildKeywords.test(text)) {
callAutotask(text);
// Also send via WS so the bot knows
vibeSendWs(text);
} else {
vibeSendWs(text);
}
}
/* ── init ── */
function initVibe() {
setupPipelineTabs();
setupSidebarCollapse();
setupWorkspaceAccordions();
// Wire up chat form
var form = document.getElementById("vibeChatForm");
if (form) form.addEventListener("submit", handleVibeSubmit);
// Connect WebSocket
connectVibeWs();
}
function setupPipelineTabs() {
var container = document.querySelector(".vibe-pipeline");
if (!container) return;
container.addEventListener("click", function (e) {
var tab = e.target.closest(".vibe-pipeline-tab");
if (!tab) return;
container.querySelectorAll(".vibe-pipeline-tab").forEach(function (t) {
t.classList.remove("active");
});
tab.classList.add("active");
});
}
function setupSidebarCollapse() {
var btn = document.getElementById("agentsSidebarCollapse");
var sidebar = document.getElementById("agentsSidebar");
if (!btn || !sidebar) return;
btn.addEventListener("click", function () {
sidebar.classList.toggle("collapsed");
btn.textContent = sidebar.classList.contains("collapsed") ? "▶" : "◀";
});
}
function setupWorkspaceAccordions() {
var toggles = document.querySelectorAll(".as-workspace-toggle");
toggles.forEach(function (toggle) {
toggle.addEventListener("click", function () {
var body = this.nextElementSibling;
var arrow = this.querySelector(".as-workspace-arrow");
if (body) {
var isOpen = body.style.display !== "none";
body.style.display = isOpen ? "none" : "";
if (arrow) arrow.textContent = isOpen ? "▶" : "▼";
}
});
});
}
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", initVibe);
} else {
initVibe();
}
})();
</script>
<style>
@keyframes nodeIn {
from {
opacity: 0;
transform: translateY(20px) scale(0.95);
}
to {
opacity: 1;
transform: translateY(0) scale(1);
}
}
@keyframes float {
0%,
100% {
transform: translateY(0);
}
50% {
transform: translateY(-10px);
}
}
.vibe-quick-btn:hover {
border-color: var(--accent) !important;
color: var(--accent) !important;
background: rgba(132, 214, 105, 0.06) !important;
transform: translateY(-2px);
}
</style>

View file

@ -1,530 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>General Bots</title>
<meta name="viewport" content="width=device-width,initial-scale=1.0" />
<meta name="description" content="General Bots - Simplified Interface" />
<meta name="theme-color" content="#3b82f6" />
<!-- Minimal styles -->
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
:root {
--primary-color: #3b82f6;
--secondary-color: #6b7280;
--background-color: #ffffff;
--text-color: #1f2937;
--border-color: #e5e7eb;
--chat-bg: #f9fafb;
--message-user-bg: #3b82f6;
--message-bot-bg: #f3f4f6;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
background: var(--background-color);
color: var(--text-color);
height: 100vh;
display: flex;
flex-direction: column;
}
/* Header */
.header {
background: var(--primary-color);
color: white;
padding: 1rem 1.5rem;
display: flex;
align-items: center;
justify-content: space-between;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.header-title {
font-size: 1.25rem;
font-weight: 600;
display: flex;
align-items: center;
gap: 0.5rem;
}
.logo {
width: 32px;
height: 32px;
background: white;
border-radius: 8px;
display: flex;
align-items: center;
justify-content: center;
font-weight: bold;
color: var(--primary-color);
}
/* Chat container */
.chat-container {
flex: 1;
display: flex;
flex-direction: column;
background: var(--chat-bg);
overflow: hidden;
}
.messages-area {
flex: 1;
padding: 1.5rem;
overflow-y: auto;
display: flex;
flex-direction: column;
gap: 1rem;
}
.message {
max-width: 70%;
word-wrap: break-word;
}
.message.user {
align-self: flex-end;
}
.message.bot {
align-self: flex-start;
}
.message-content {
padding: 0.75rem 1rem;
border-radius: 12px;
line-height: 1.5;
}
.message.user .message-content {
background: var(--message-user-bg);
color: white;
border-bottom-right-radius: 4px;
}
.message.bot .message-content {
background: var(--message-bot-bg);
color: var(--text-color);
border-bottom-left-radius: 4px;
border: 1px solid var(--border-color);
}
.message-time {
font-size: 0.75rem;
color: var(--secondary-color);
margin-top: 0.25rem;
padding: 0 0.5rem;
}
.message.user .message-time {
text-align: right;
}
/* Input area */
.input-container {
background: white;
border-top: 1px solid var(--border-color);
padding: 1rem 1.5rem;
display: flex;
gap: 0.75rem;
}
.input-wrapper {
flex: 1;
display: flex;
align-items: center;
background: var(--chat-bg);
border: 1px solid var(--border-color);
border-radius: 24px;
padding: 0.5rem 1rem;
transition: border-color 0.2s;
}
.input-wrapper:focus-within {
border-color: var(--primary-color);
}
#messageInput {
flex: 1;
border: none;
outline: none;
background: transparent;
font-size: 1rem;
line-height: 1.5;
}
.send-button {
background: var(--primary-color);
color: white;
border: none;
border-radius: 50%;
width: 40px;
height: 40px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: background-color 0.2s;
}
.send-button:hover {
background: #2563eb;
}
.send-button:disabled {
background: var(--secondary-color);
cursor: not-allowed;
}
/* Loading indicator */
.typing-indicator {
display: none;
align-self: flex-start;
padding: 0.75rem 1rem;
background: var(--message-bot-bg);
border: 1px solid var(--border-color);
border-radius: 12px;
border-bottom-left-radius: 4px;
}
.typing-indicator.show {
display: block;
}
.typing-dots {
display: flex;
gap: 4px;
}
.typing-dots span {
width: 8px;
height: 8px;
background: var(--secondary-color);
border-radius: 50%;
animation: typing 1.4s infinite;
}
.typing-dots span:nth-child(2) {
animation-delay: 0.2s;
}
.typing-dots span:nth-child(3) {
animation-delay: 0.4s;
}
@keyframes typing {
0%, 60%, 100% {
transform: translateY(0);
opacity: 0.7;
}
30% {
transform: translateY(-10px);
opacity: 1;
}
}
/* Scrollbar styling */
.messages-area::-webkit-scrollbar {
width: 6px;
}
.messages-area::-webkit-scrollbar-track {
background: transparent;
}
.messages-area::-webkit-scrollbar-thumb {
background: var(--border-color);
border-radius: 3px;
}
.messages-area::-webkit-scrollbar-thumb:hover {
background: var(--secondary-color);
}
/* Responsive design */
@media (max-width: 768px) {
.message {
max-width: 85%;
}
.header {
padding: 0.75rem 1rem;
}
.messages-area {
padding: 1rem;
}
.input-container {
padding: 0.75rem 1rem;
}
}
/* Dark mode support */
@media (prefers-color-scheme: dark) {
:root {
--background-color: #111827;
--text-color: #f9fafb;
--border-color: #374151;
--chat-bg: #1f2937;
--message-bot-bg: #374151;
}
.header {
background: #1f2937;
border-bottom: 1px solid var(--border-color);
}
.input-container {
background: #111827;
}
.input-wrapper {
background: #1f2937;
}
}
</style>
</head>
<body>
<!-- Header -->
<header class="header">
<div class="header-title">
<div class="logo">GB</div>
<span>General Bots</span>
</div>
<div id="connectionStatus"></div>
</header>
<!-- Main chat container -->
<div class="chat-container">
<!-- Messages area -->
<div class="messages-area" id="messagesArea">
<!-- Welcome message -->
<div class="message bot">
<div class="message-content">
Hello! I'm your General Bots assistant. How can I help you today?
</div>
<div class="message-time">Just now</div>
</div>
</div>
<!-- Typing indicator -->
<div class="typing-indicator" id="typingIndicator">
<div class="typing-dots">
<span></span>
<span></span>
<span></span>
</div>
</div>
</div>
<!-- Input area -->
<div class="input-container">
<div class="input-wrapper">
<input
type="text"
id="messageInput"
placeholder="Type your message..."
aria-label="Message input"
autocomplete="off"
/>
</div>
<button class="send-button" id="sendButton" aria-label="Send message">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M22 2L11 13M22 2l-7 20-4-9-9-4z"/>
</svg>
</button>
</div>
<!-- Simple chat script -->
<script>
(function() {
'use strict';
// Elements
const messagesArea = document.getElementById('messagesArea');
const messageInput = document.getElementById('messageInput');
const sendButton = document.getElementById('sendButton');
const typingIndicator = document.getElementById('typingIndicator');
// WebSocket connection
let ws = null;
let reconnectTimeout = null;
// Initialize WebSocket
function initWebSocket() {
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
const wsUrl = `${protocol}//${window.location.host}/ws`;
try {
ws = new WebSocket(wsUrl);
ws.onopen = () => {
console.log('WebSocket connected');
updateConnectionStatus(true);
};
ws.onmessage = (event) => {
const data = JSON.parse(event.data);
handleIncomingMessage(data);
};
ws.onclose = () => {
console.log('WebSocket disconnected');
updateConnectionStatus(false);
scheduleReconnect();
};
ws.onerror = (error) => {
console.error('WebSocket error:', error);
};
} catch (error) {
console.error('Failed to create WebSocket:', error);
scheduleReconnect();
}
}
// Reconnect logic
function scheduleReconnect() {
if (reconnectTimeout) return;
reconnectTimeout = setTimeout(() => {
reconnectTimeout = null;
initWebSocket();
}, 3000);
}
// Update connection status
function updateConnectionStatus(connected) {
const statusEl = document.getElementById('connectionStatus');
if (statusEl) {
statusEl.textContent = connected ? '● Connected' : '○ Disconnected';
statusEl.style.color = connected ? '#10b981' : '#ef4444';
}
}
// Handle incoming messages
function handleIncomingMessage(data) {
typingIndicator.classList.remove('show');
if (data.content) {
addMessage(data.content, 'bot');
}
}
// Add message to chat
function addMessage(text, sender) {
const messageDiv = document.createElement('div');
messageDiv.className = `message ${sender}`;
const contentDiv = document.createElement('div');
contentDiv.className = 'message-content';
contentDiv.textContent = text;
const timeDiv = document.createElement('div');
timeDiv.className = 'message-time';
timeDiv.textContent = formatTime(new Date());
messageDiv.appendChild(contentDiv);
messageDiv.appendChild(timeDiv);
messagesArea.appendChild(messageDiv);
scrollToBottom();
}
// Format timestamp
function formatTime(date) {
return date.toLocaleTimeString('en-US', {
hour: 'numeric',
minute: '2-digit',
hour12: true
});
}
// Scroll to bottom
function scrollToBottom() {
messagesArea.scrollTop = messagesArea.scrollHeight;
}
// Send message
function sendMessage() {
const message = messageInput.value.trim();
if (!message) return;
// Add user message
addMessage(message, 'user');
// Clear input
messageInput.value = '';
// Show typing indicator
typingIndicator.classList.add('show');
scrollToBottom();
// Send via WebSocket if connected
if (ws && ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify({
type: 'message',
content: message
}));
} else {
// Fallback: Send via HTTP
sendViaHttp(message);
}
}
// Fallback HTTP sending
async function sendViaHttp(message) {
try {
const response = await fetch('/api/chat/message', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ content: message })
});
if (response.ok) {
const data = await response.json();
handleIncomingMessage(data);
}
} catch (error) {
console.error('Failed to send message:', error);
typingIndicator.classList.remove('show');
addMessage('Sorry, I couldn\'t process your message. Please try again.', 'bot');
}
}
// Event listeners
sendButton.addEventListener('click', sendMessage);
messageInput.addEventListener('keypress', (e) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
sendMessage();
}
});
// Initialize on load
document.addEventListener('DOMContentLoaded', () => {
initWebSocket();
messageInput.focus();
});
// Handle page visibility
document.addEventListener('visibilitychange', () => {
if (!document.hidden && (!ws || ws.readyState !== WebSocket.OPEN)) {
initWebSocket();
}
});
})();
</script>
</body>
</html>

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -1,7 +1,7 @@
/* =============================================================================
AUTOTASK - SENTIENT THEME
Dark UI with Neon Yellow/Lime Accents (#CDFE00)
Pixel-perfect match to Mantis Farm Dashboard
Pixel-perfect match to Agent Dashboard
============================================================================= */
/* =============================================================================
@ -25,7 +25,7 @@
--sentient-text-primary: var(--text, #ffffff);
--sentient-text-secondary: var(--text-secondary, #a0a0a0);
--sentient-text-muted: var(--text-tertiary, #666666);
--sentient-text-dark: #000000;
--sentient-text-dark: var(--bg-invert, #000000);
--sentient-border: var(--border, #2a2a2a);
--sentient-border-light: var(--border-light, #333333);
@ -1166,10 +1166,12 @@ body:has(.autotask-container) {
}
@keyframes blink {
0%,
100% {
opacity: 1;
}
50% {
opacity: 0;
}
@ -1224,10 +1226,12 @@ body:has(.autotask-container) {
}
@keyframes pulse-glow {
0%,
100% {
filter: drop-shadow(0 0 4px var(--sentient-accent-glow));
}
50% {
filter: drop-shadow(0 0 12px var(--sentient-accent));
}
@ -1335,6 +1339,7 @@ body:has(.autotask-container) {
opacity: 0;
transform: translateY(-4px);
}
to {
opacity: 1;
transform: translateY(0);
@ -1596,6 +1601,7 @@ body:has(.autotask-container) {
opacity: 0;
transform: translateX(20px);
}
to {
opacity: 1;
transform: translateX(0);
@ -1709,4 +1715,4 @@ body:has(.autotask-container) {
width: auto;
bottom: 12px;
}
}
}

View file

@ -0,0 +1,555 @@
.progress-panel {
display: flex;
flex-direction: column;
gap: 24px;
padding: 20px;
background: var(--bg-primary, #0a0a0f);
color: var(--text-primary, #e0e0e0);
font-family:
"Inter",
-apple-system,
BlinkMacSystemFont,
sans-serif;
border-radius: 12px;
border: 1px solid var(--border-color, #1a1a24);
height: 100%;
min-height: 0;
overflow: hidden;
}
.status-section {
background: var(--bg-secondary, #111118);
border-radius: 8px;
padding: 16px 20px;
border: 1px solid var(--border-color, #1a1a24);
flex-shrink: 0;
min-height: 120px;
}
.status-header {
margin-bottom: 12px;
}
.status-label {
font-size: 11px;
font-weight: 600;
letter-spacing: 1.2px;
color: var(--text-muted, #666);
text-transform: uppercase;
}
.status-content {
display: flex;
flex-direction: column;
gap: 8px;
}
.status-title-row {
display: flex;
align-items: center;
justify-content: space-between;
}
.status-title {
font-size: 16px;
font-weight: 500;
color: var(--text-primary, #e0e0e0);
}
.status-time {
display: flex;
align-items: center;
gap: 8px;
font-size: 13px;
}
.runtime-label,
.estimated-label {
color: var(--text-muted, #666);
}
.runtime-value,
.estimated-value {
color: var(--text-primary, #e0e0e0);
}
.status-indicator {
width: 8px;
height: 8px;
border-radius: 50%;
background: var(--text-muted, #666);
}
.status-indicator.active {
background: var(--accent-yellow, #d4e94c);
box-shadow: 0 0 8px var(--accent-yellow, #d4e94c);
}
.status-current-action {
display: flex;
align-items: center;
gap: 10px;
padding: 8px 0;
}
.action-dot {
width: 8px;
height: 8px;
border-radius: 50%;
background: var(--text-muted, #666);
flex-shrink: 0;
}
.action-dot.active {
background: var(--accent-yellow, #d4e94c);
}
.action-text {
font-size: 14px;
color: var(--text-primary, #e0e0e0);
flex: 1;
}
.estimated-time {
display: flex;
align-items: center;
gap: 8px;
font-size: 13px;
margin-left: auto;
}
.settings-icon {
color: var(--text-muted, #666);
cursor: pointer;
transition: color 0.2s;
}
.settings-icon:hover {
color: var(--text-primary, #e0e0e0);
}
.status-decision-point {
display: flex;
align-items: center;
gap: 10px;
padding: 8px 0;
opacity: 0.7;
}
.decision-dot {
width: 8px;
height: 8px;
border-radius: 50%;
background: var(--text-muted, #444);
flex-shrink: 0;
}
.decision-dot.pending {
background: var(--text-muted, #444);
border: 1px dashed var(--text-muted, #666);
}
.decision-text {
font-size: 13px;
color: var(--text-muted, #888);
}
.decision-badge {
font-size: 12px;
padding: 4px 10px;
background: var(--bg-tertiary, #1a1a24);
border-radius: 4px;
color: var(--text-muted, #888);
margin-left: auto;
}
.progress-log-section {
background: var(--bg-secondary, #111118);
border-radius: 8px;
border: 1px solid var(--border-color, #1a1a24);
overflow: hidden;
flex: 1;
min-height: 0;
display: flex;
flex-direction: column;
}
.progress-log-header {
padding: 14px 20px;
border-bottom: 1px solid var(--border-color, #1a1a24);
flex-shrink: 0;
}
.progress-log-label {
font-size: 11px;
font-weight: 600;
letter-spacing: 1.2px;
color: var(--text-muted, #666);
text-transform: uppercase;
}
.progress-log-content {
padding: 0;
flex: 1;
min-height: 0;
overflow-y: auto;
overflow-x: hidden;
}
.log-section {
border-bottom: 1px solid var(--border-color, #1a1a24);
min-height: 48px;
}
.log-section:last-child {
border-bottom: none;
}
.log-section-header {
display: flex;
align-items: center;
gap: 12px;
padding: 14px 20px;
cursor: pointer;
transition: background 0.2s;
min-height: 48px;
}
.log-section-header:hover {
background: var(--bg-hover, #151520);
}
.section-indicator {
width: 10px;
height: 10px;
border-radius: 50%;
flex-shrink: 0;
}
.section-indicator.completed {
background: var(--accent-yellow, #d4e94c);
}
.section-indicator.running {
background: var(--accent-blue, #4c9ee9);
animation: pulse 1.5s infinite;
}
.section-indicator.pending {
background: var(--text-muted, #444);
}
.section-indicator.failed {
background: var(--accent-red, #e94c4c);
}
@keyframes pulse {
0%,
100% {
opacity: 1;
}
50% {
opacity: 0.5;
}
}
.section-name {
font-size: 14px;
font-weight: 500;
color: var(--text-primary, #e0e0e0);
}
.section-details-link {
font-size: 12px;
color: var(--text-muted, #666);
cursor: pointer;
transition: color 0.2s;
}
.section-details-link:hover {
color: var(--accent-yellow, #d4e94c);
}
.section-step-badge {
margin-left: auto;
font-size: 12px;
padding: 4px 10px;
background: var(--accent-yellow, #d4e94c);
color: var(--bg-primary, #0a0a0f);
border-radius: 4px;
font-weight: 600;
}
.section-status-badge {
font-size: 12px;
padding: 4px 10px;
border-radius: 4px;
font-weight: 500;
}
.section-status-badge.completed {
background: transparent;
color: var(--text-muted, #888);
}
.section-status-badge.running {
background: var(--accent-blue, #4c9ee9);
color: var(--bg-primary, #0a0a0f);
}
.section-status-badge.pending {
background: var(--bg-tertiary, #1a1a24);
color: var(--text-muted, #666);
}
.log-section-body {
display: none;
padding-left: 20px;
background: var(--bg-tertiary, #0d0d14);
}
.log-section.expanded .log-section-body {
display: block;
}
.log-children {
padding: 0;
}
.log-child {
border-bottom: 1px solid var(--border-subtle, #151520);
}
.log-child:last-child {
border-bottom: none;
}
.log-child-header {
display: flex;
align-items: center;
gap: 12px;
padding: 12px 20px 12px 32px;
cursor: pointer;
transition: background 0.2s;
}
.log-child-header:hover {
background: var(--bg-hover, #131320);
}
.child-indent {
width: 16px;
height: 1px;
background: var(--border-color, #1a1a24);
flex-shrink: 0;
}
.child-name {
font-size: 13px;
color: var(--text-secondary, #aaa);
}
.child-details-link {
font-size: 11px;
color: var(--text-muted, #555);
cursor: pointer;
transition: color 0.2s;
}
.child-details-link:hover {
color: var(--accent-yellow, #d4e94c);
}
.child-step-badge {
margin-left: auto;
font-size: 11px;
padding: 3px 8px;
background: var(--accent-yellow, #d4e94c);
color: var(--bg-primary, #0a0a0f);
border-radius: 4px;
font-weight: 600;
}
.child-status-badge {
font-size: 11px;
padding: 3px 8px;
border-radius: 4px;
}
.child-status-badge.completed {
color: var(--text-muted, #888);
}
.log-child-body {
display: none;
padding-left: 48px;
}
.log-child.expanded .log-child-body {
display: block;
}
.log-items {
padding: 8px 0;
}
.log-item {
display: flex;
align-items: center;
gap: 10px;
padding: 8px 20px;
font-size: 12px;
}
.item-dot {
width: 6px;
height: 6px;
border-radius: 50%;
flex-shrink: 0;
}
.item-dot.completed {
background: var(--accent-yellow, #d4e94c);
}
.item-dot.running {
background: var(--accent-blue, #4c9ee9);
}
.item-dot.pending {
background: var(--text-muted, #444);
}
.item-name {
color: var(--text-secondary, #999);
flex: 1;
}
.item-info {
display: flex;
align-items: center;
gap: 8px;
margin-left: auto;
}
.item-duration {
font-size: 11px;
color: var(--text-muted, #666);
}
.item-check {
font-size: 14px;
}
.item-check.completed {
color: var(--accent-green, #4ce97a);
}
.item-check.running {
color: var(--accent-blue, #4c9ee9);
}
.terminal-section {
background: var(--bg-terminal, #0a0a0f);
border-radius: 8px;
border: 1px solid var(--border-color, #1a1a24);
overflow: hidden;
flex-shrink: 0;
min-height: 150px;
max-height: 250px;
display: flex;
flex-direction: column;
}
.terminal-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 14px 20px;
border-bottom: 1px solid var(--border-color, #1a1a24);
background: var(--bg-secondary, #111118);
flex-shrink: 0;
}
.terminal-label {
font-size: 11px;
font-weight: 600;
letter-spacing: 1.2px;
color: var(--text-muted, #666);
text-transform: uppercase;
}
.terminal-stats {
display: flex;
align-items: center;
gap: 4px;
font-size: 12px;
color: var(--text-muted, #666);
}
.terminal-stats strong {
color: var(--text-primary, #e0e0e0);
font-weight: 500;
}
.stat-separator {
margin: 0 8px;
color: var(--text-muted, #444);
}
.terminal-content {
padding: 16px 20px;
font-family: "JetBrains Mono", "Fira Code", "Monaco", monospace;
font-size: 13px;
line-height: 1.6;
flex: 1;
min-height: 0;
overflow-y: auto;
background: var(--bg-terminal, #0a0a0f);
}
.terminal-line {
color: var(--text-terminal, #8b8b8b);
padding: 2px 0;
}
.terminal-line.info {
color: var(--text-terminal, #8b8b8b);
}
.terminal-line.success {
color: var(--accent-green, #4ce97a);
}
.terminal-line.error {
color: var(--accent-red, #e94c4c);
}
.terminal-line.warning {
color: var(--accent-yellow, #d4e94c);
}
.terminal-line.progress {
color: var(--accent-blue, #4c9ee9);
}
.progress-log-content::-webkit-scrollbar,
.terminal-content::-webkit-scrollbar {
width: 6px;
}
.progress-log-content::-webkit-scrollbar-track,
.terminal-content::-webkit-scrollbar-track {
background: var(--bg-tertiary, #0d0d14);
}
.progress-log-content::-webkit-scrollbar-thumb,
.terminal-content::-webkit-scrollbar-thumb {
background: var(--border-color, #1a1a24);
border-radius: 3px;
}
.progress-log-content::-webkit-scrollbar-thumb:hover,
.terminal-content::-webkit-scrollbar-thumb:hover {
background: var(--text-muted, #444);
}

View file

@ -0,0 +1,553 @@
const ProgressPanel = {
manifest: null,
wsConnection: null, // Deprecated - now uses singleton from tasks.js
startTime: null,
runtimeInterval: null,
_boundHandler: null, // Store bound handler for cleanup
init(taskId) {
// Clean up any existing handler before registering a new one
// This prevents duplicate handlers if init is called multiple times
if (
this._boundHandler &&
typeof unregisterTaskProgressHandler === "function"
) {
unregisterTaskProgressHandler(this._boundHandler);
this._boundHandler = null;
}
this.taskId = taskId;
this.startTime = Date.now();
this.startRuntimeCounter();
this.connectWebSocket(taskId);
},
connectWebSocket(taskId) {
// Instead of creating our own WebSocket, register with the singleton from tasks.js
// This prevents the "2 receivers" problem where manifest_update goes to one connection
// while the browser UI is listening on another
console.log("[ProgressPanel] Using singleton WebSocket for task:", taskId);
// Create bound handler that filters for our task
this._boundHandler = (data) => {
// Only process messages for our task
if (data.task_id && String(data.task_id) !== String(taskId)) {
return;
}
this.handleProgressUpdate(data);
};
// Register with the global singleton WebSocket
if (typeof registerTaskProgressHandler === "function") {
registerTaskProgressHandler(this._boundHandler);
console.log("[ProgressPanel] Registered with singleton WebSocket");
} else {
// Fallback: wait for tasks.js to load and retry
console.log(
"[ProgressPanel] Waiting for tasks.js singleton to be available...",
);
setTimeout(() => this.connectWebSocket(taskId), 500);
}
},
handleProgressUpdate(data) {
// Skip manifest_update - already handled by tasks.js renderManifestProgress()
// Processing it here would cause duplicate updates and race conditions
if (
data.type === "manifest_update" ||
data.event_type === "manifest_update"
) {
// Don't process here - tasks.js handles this via handleWebSocketMessage()
// which calls renderManifestProgress() with proper normalized ID handling
return;
}
if (data.type === "section_update") {
this.updateSection(data.section_id, data.status, data.progress);
} else if (data.type === "item_update") {
this.updateItem(
data.section_id,
data.item_id,
data.status,
data.duration,
);
} else if (data.type === "terminal_line") {
this.addTerminalLine(data.content, data.line_type);
} else if (data.type === "stats_update") {
this.updateStats(data.stats);
} else if (data.type === "task_progress") {
this.handleTaskProgress(data);
}
},
handleTaskProgress(data) {
// Check for manifest in activity
if (data.activity && data.activity.manifest) {
this.manifest = data.activity.manifest;
this.render();
}
// Also check for manifest in details (manifest_update events)
if (
data.details &&
(data.step === "manifest_update" || data.event_type === "manifest_update")
) {
try {
const parsed =
typeof data.details === "string"
? JSON.parse(data.details)
: data.details;
if (parsed && parsed.sections) {
this.manifest = parsed;
this.render();
}
} catch (e) {
// Not a manifest JSON, might be terminal output
console.debug("Details is not manifest JSON:", e.message);
}
}
if (data.step && data.step !== "manifest_update") {
this.updateCurrentAction(data.message || data.step);
}
// Only add non-manifest details as terminal lines
if (
data.details &&
data.step !== "manifest_update" &&
data.event_type !== "manifest_update"
) {
this.addTerminalLine(data.details, "info");
}
},
startRuntimeCounter() {
this.runtimeInterval = setInterval(() => {
const elapsed = Math.floor((Date.now() - this.startTime) / 1000);
const runtimeEl = document.getElementById("status-runtime");
if (runtimeEl) {
runtimeEl.textContent = this.formatDuration(elapsed);
}
}, 1000);
},
stopRuntimeCounter() {
if (this.runtimeInterval) {
clearInterval(this.runtimeInterval);
this.runtimeInterval = null;
}
},
formatDuration(seconds) {
if (seconds < 60) {
return `${seconds} sec`;
} else if (seconds < 3600) {
const mins = Math.floor(seconds / 60);
return `${mins} min`;
} else {
const hours = Math.floor(seconds / 3600);
const mins = Math.floor((seconds % 3600) / 60);
return `${hours} hr ${mins} min`;
}
},
render() {
if (!this.manifest) return;
this.renderStatus();
this.renderProgressLog();
this.renderTerminal();
},
renderStatus() {
const titleEl = document.getElementById("status-title");
if (titleEl) {
titleEl.textContent = this.manifest.description || this.manifest.app_name;
}
const estimatedEl = document.getElementById("estimated-time");
if (estimatedEl && this.manifest.estimated_seconds) {
estimatedEl.textContent = this.formatDuration(
this.manifest.estimated_seconds,
);
}
const currentAction = this.getCurrentAction();
const actionEl = document.getElementById("current-action");
if (actionEl && currentAction) {
actionEl.textContent = currentAction;
}
this.updateDecisionPoint();
},
getCurrentAction() {
if (!this.manifest || !this.manifest.sections) return null;
for (const section of this.manifest.sections) {
if (section.status === "Running") {
for (const child of section.children || []) {
if (child.status === "Running") {
for (const item of child.items || []) {
if (item.status === "Running") {
return item.name;
}
}
return child.name;
}
}
return section.name;
}
}
return null;
},
updateCurrentAction(action) {
const actionEl = document.getElementById("current-action");
if (actionEl) {
actionEl.textContent = action;
}
},
updateDecisionPoint() {
const decisionStepEl = document.getElementById("decision-step");
const decisionTotalEl = document.getElementById("decision-total");
if (decisionStepEl && this.manifest) {
decisionStepEl.textContent = this.manifest.completed_steps || 0;
}
if (decisionTotalEl && this.manifest) {
decisionTotalEl.textContent = this.manifest.total_steps || 0;
}
},
renderProgressLog() {
const container = document.getElementById("progress-log-content");
if (!container || !this.manifest || !this.manifest.sections) return;
container.innerHTML = "";
for (const section of this.manifest.sections) {
const sectionEl = this.createSectionElement(section);
container.appendChild(sectionEl);
}
},
createSectionElement(section) {
const sectionDiv = document.createElement("div");
sectionDiv.className = "log-section";
sectionDiv.dataset.sectionId = section.id;
if (section.status === "Running" || section.status === "Completed") {
sectionDiv.classList.add("expanded");
}
const statusClass = section.status.toLowerCase();
// Support both direct fields and nested progress object
const stepCurrent = section.current_step ?? section.progress?.current ?? 0;
const stepTotal = section.total_steps ?? section.progress?.total ?? 0;
sectionDiv.innerHTML = `
<div class="log-section-header" onclick="ProgressPanel.toggleSection('${section.id}')">
<span class="section-indicator ${statusClass}"></span>
<span class="section-name">${this.escapeHtml(section.name)}</span>
<span class="section-details-link" onclick="event.stopPropagation(); ProgressPanel.viewDetails('${section.id}')">View Details </span>
<span class="section-step-badge">Step ${stepCurrent}/${stepTotal}</span>
<span class="section-status-badge ${statusClass}">${section.status}</span>
</div>
<div class="log-section-body">
<div class="log-children" id="log-children-${section.id}">
</div>
</div>
`;
const childrenContainer = sectionDiv.querySelector(".log-children");
for (const child of section.children || []) {
const childEl = this.createChildElement(child, section.id);
childrenContainer.appendChild(childEl);
}
if (
section.items &&
section.items.length > 0 &&
(!section.children || section.children.length === 0)
) {
for (const item of section.items) {
const itemEl = this.createItemElement(item);
childrenContainer.appendChild(itemEl);
}
}
return sectionDiv;
},
createChildElement(child, parentId) {
const childDiv = document.createElement("div");
childDiv.className = "log-child";
childDiv.dataset.childId = child.id;
if (child.status === "Running" || child.status === "Completed") {
childDiv.classList.add("expanded");
}
const statusClass = child.status.toLowerCase();
// Support both direct fields and nested progress object
const stepCurrent = child.current_step ?? child.progress?.current ?? 0;
const stepTotal = child.total_steps ?? child.progress?.total ?? 0;
const duration = child.duration_seconds
? this.formatDuration(child.duration_seconds)
: "";
childDiv.innerHTML = `
<div class="log-child-header" onclick="ProgressPanel.toggleChild('${child.id}')">
<span class="child-indent"></span>
<span class="child-name">${this.escapeHtml(child.name)}</span>
<span class="child-details-link" onclick="event.stopPropagation(); ProgressPanel.viewChildDetails('${child.id}')">View Details </span>
<span class="child-step-badge">Step ${stepCurrent}/${stepTotal}</span>
<span class="child-status-badge ${statusClass}">${child.status}</span>
</div>
<div class="log-child-body">
<div class="log-items" id="log-items-${child.id}">
</div>
</div>
`;
const itemsContainer = childDiv.querySelector(".log-items");
for (const item of child.items || []) {
const itemEl = this.createItemElement(item);
itemsContainer.appendChild(itemEl);
}
return childDiv;
},
createItemElement(item) {
const itemDiv = document.createElement("div");
itemDiv.className = "log-item";
itemDiv.dataset.itemId = item.id;
const statusClass = item.status.toLowerCase();
const duration = item.duration_seconds
? `Duration: ${this.formatDuration(item.duration_seconds)}`
: "";
const checkIcon =
item.status === "Completed" ? "✓" : item.status === "Running" ? "◎" : "○";
itemDiv.innerHTML = `
<span class="item-dot ${statusClass}"></span>
<span class="item-name">${this.escapeHtml(item.name)}${item.details ? ` - ${this.escapeHtml(item.details)}` : ""}</span>
<div class="item-info">
<span class="item-duration">${duration}</span>
<span class="item-check ${statusClass}">${checkIcon}</span>
</div>
`;
return itemDiv;
},
renderTerminal() {
// Support both formats: terminal_output (direct) and terminal.lines (web JSON)
const terminalLines =
this.manifest?.terminal_output || this.manifest?.terminal?.lines || [];
if (!terminalLines.length) return;
const container = document.getElementById("terminal-content");
if (!container) return;
container.innerHTML = "";
for (const line of terminalLines.slice(-50)) {
this.appendTerminalLine(
container,
line.content,
line.type || line.line_type || "info",
);
}
container.scrollTop = container.scrollHeight;
},
addTerminalLine(content, lineType) {
const container = document.getElementById("terminal-content");
if (!container) return;
this.appendTerminalLine(container, content, lineType);
container.scrollTop = container.scrollHeight;
this.incrementProcessedCount();
},
appendTerminalLine(container, content, lineType) {
const lineDiv = document.createElement("div");
lineDiv.className = `terminal-line ${lineType || "info"}`;
lineDiv.textContent = content;
container.appendChild(lineDiv);
},
incrementProcessedCount() {
const processedEl = document.getElementById("terminal-processed");
if (processedEl) {
const current = parseInt(processedEl.textContent, 10) || 0;
processedEl.textContent = current + 1;
}
},
updateStats(stats) {
const processedEl = document.getElementById("terminal-processed");
if (processedEl && stats.data_points_processed !== undefined) {
processedEl.textContent = stats.data_points_processed;
}
const speedEl = document.getElementById("terminal-speed");
if (speedEl && stats.sources_per_min !== undefined) {
speedEl.textContent = `~${stats.sources_per_min.toFixed(1)} sources/min`;
}
const etaEl = document.getElementById("terminal-eta");
if (etaEl && stats.estimated_remaining_seconds !== undefined) {
etaEl.textContent = this.formatDuration(
stats.estimated_remaining_seconds,
);
}
},
updateSection(sectionId, status, progress) {
const sectionEl = document.getElementById("window-tasks").querySelector(
`[data-section-id="${sectionId}"]`,
);
if (!sectionEl) return;
const indicator = sectionEl.querySelector(".section-indicator");
const statusBadge = sectionEl.querySelector(".section-status-badge");
const stepBadge = sectionEl.querySelector(".section-step-badge");
if (indicator) {
indicator.className = `section-indicator ${status.toLowerCase()}`;
}
if (statusBadge) {
statusBadge.className = `section-status-badge ${status.toLowerCase()}`;
statusBadge.textContent = status;
}
if (stepBadge && progress) {
stepBadge.textContent = `Step ${progress.current}/${progress.total}`;
}
if (status === "Running" || status === "Completed") {
sectionEl.classList.add("expanded");
}
},
updateItem(sectionId, itemId, status, duration) {
const itemEl = document.getElementById("window-tasks").querySelector(`[data-item-id="${itemId}"]`);
if (!itemEl) return;
const dot = itemEl.querySelector(".item-dot");
const check = itemEl.querySelector(".item-check");
const durationEl = itemEl.querySelector(".item-duration");
const statusClass = status.toLowerCase();
if (dot) {
dot.className = `item-dot ${statusClass}`;
}
if (check) {
check.className = `item-check ${statusClass}`;
check.textContent =
status === "Completed" ? "✓" : status === "Running" ? "◎" : "○";
}
if (durationEl && duration) {
durationEl.textContent = `Duration: ${this.formatDuration(duration)}`;
}
},
toggleSection(sectionId) {
const sectionEl = document.getElementById("window-tasks").querySelector(
`[data-section-id="${sectionId}"]`,
);
if (sectionEl) {
sectionEl.classList.toggle("expanded");
}
},
toggleChild(childId) {
const childEl = document.getElementById("window-tasks").querySelector(`[data-child-id="${childId}"]`);
if (childEl) {
childEl.classList.toggle("expanded");
}
},
viewDetails(sectionId) {
console.log("View details for section:", sectionId);
},
viewChildDetails(childId) {
console.log("View details for child:", childId);
},
escapeHtml(text) {
const div = document.createElement("div");
div.textContent = text;
return div.innerHTML;
},
loadManifest(taskId) {
fetch(`/api/autotask/${taskId}/manifest`)
.then((response) => response.json())
.then((data) => {
if (data.success && data.manifest) {
this.manifest = data.manifest;
this.render();
}
})
.catch((error) => {
console.error("Failed to load manifest:", error);
});
},
destroy() {
this.stopRuntimeCounter();
// Unregister from singleton instead of closing our own connection
if (
this._boundHandler &&
typeof unregisterTaskProgressHandler === "function"
) {
unregisterTaskProgressHandler(this._boundHandler);
this._boundHandler = null;
console.log("[ProgressPanel] Unregistered from singleton WebSocket");
}
// Don't close the singleton connection - other components may be using it
this.wsConnection = null;
},
};
function toggleLogSection(header) {
const section = header.closest(".log-section");
if (section) {
section.classList.toggle("expanded");
}
}
function toggleLogChild(header) {
const child = header.closest(".log-child");
if (child) {
child.classList.toggle("expanded");
}
}
function viewSectionDetails(sectionId) {
ProgressPanel.viewDetails(sectionId);
}
function viewChildDetails(childId) {
ProgressPanel.viewChildDetails(childId);
}
window.ProgressPanel = ProgressPanel;

View file

@ -0,0 +1,411 @@
/* Task Window — Unified Tasks Dashboard Styles */
.tw-container {
display: flex;
flex-direction: column;
height: 100%;
background: var(--bg);
font-family: 'Fira Code', monospace;
}
/* ============================================
TAB HEADER
============================================ */
.tw-tabs-header {
display: flex;
background: var(--surface);
border-bottom: 1px solid var(--border);
min-height: 34px;
overflow-x: auto;
}
.tw-tab {
height: 34px;
min-width: 140px;
padding: 0 18px;
border: none;
border-right: 1px solid var(--border);
background: transparent;
font-family: 'Fira Code', monospace;
font-size: 12px;
font-weight: 600;
color: var(--text);
cursor: pointer;
display: flex;
align-items: center;
gap: 4px;
transition: all 0.15s;
white-space: nowrap;
}
.tw-tab:hover {
background: var(--bg);
}
.tw-tab.active {
background: var(--accent);
color: var(--bg);
}
.tw-tab-prefix {
opacity: 0.7;
}
.tw-tab .tw-tab-close {
margin-left: 8px;
opacity: 0.5;
font-size: 14px;
cursor: pointer;
}
.tw-tab .tw-tab-close:hover {
opacity: 1;
}
/* ============================================
PIPELINE TABS
============================================ */
.tw-pipeline-tabs {
display: flex;
background: var(--surface);
border-bottom: 1px solid var(--border);
}
.tw-pipeline-tab {
flex: 1;
height: 32px;
border: none;
border-right: 1px solid var(--border);
background: transparent;
font-family: 'Fira Code', monospace;
font-size: 11px;
font-weight: 500;
color: var(--text-muted);
cursor: pointer;
transition: all 0.15s;
}
.tw-pipeline-tab:last-child {
border-right: none;
}
.tw-pipeline-tab:hover {
background: var(--bg);
color: var(--text);
}
.tw-pipeline-tab.active {
background: var(--accent);
color: var(--bg);
}
/* ============================================
FILTERS
============================================ */
.tw-filters {
display: flex;
gap: 8px;
padding: 12px 16px;
border-bottom: 1px solid var(--border);
}
.tw-filter-chip {
display: inline-flex;
align-items: center;
gap: 4px;
padding: 5px 12px;
border: 1px solid var(--border);
border-radius: 16px;
background: var(--bg);
font-family: 'Fira Code', monospace;
font-size: 11px;
font-weight: 500;
color: var(--text-muted);
cursor: pointer;
transition: all 0.15s;
}
.tw-filter-chip:hover {
border-color: var(--accent);
color: var(--text);
}
.tw-filter-chip.active {
background: var(--accent);
color: var(--bg);
border-color: var(--accent);
}
.tw-filter-icon {
font-size: 10px;
}
/* ============================================
INTENT BAR
============================================ */
.tw-intent-bar {
display: flex;
gap: 8px;
padding: 12px 16px;
border-bottom: 1px solid var(--border);
}
.tw-intent-input {
flex: 1;
padding: 8px 14px;
border: 1px solid var(--border);
border-radius: 6px;
font-family: 'Fira Code', monospace;
font-size: 13px;
color: var(--text);
outline: none;
transition: border-color 0.15s;
background: var(--bg);
}
.tw-intent-input:focus {
border-color: var(--accent);
box-shadow: 0 0 0 3px var(--accent-light, rgba(132, 214, 105, 0.15));
}
.tw-intent-input::placeholder {
color: var(--text-muted);
}
.tw-intent-run {
padding: 8px 20px;
border: none;
border-radius: 6px;
background: var(--accent);
color: var(--bg);
font-family: 'Fira Code', monospace;
font-size: 12px;
font-weight: 700;
cursor: pointer;
transition: all 0.15s;
letter-spacing: 0.5px;
}
.tw-intent-run:hover {
background: var(--accent-hover, #72c458);
transform: translateY(-1px);
box-shadow: 0 2px 8px var(--accent-light, rgba(132, 214, 105, 0.3));
}
.tw-intent-run:disabled {
opacity: 0.6;
cursor: not-allowed;
transform: none;
}
/* ============================================
SPLIT VIEW (task list + detail)
============================================ */
.tw-split-view {
flex: 1;
display: flex;
overflow: hidden;
}
.tw-task-list {
width: 280px;
min-width: 200px;
border-right: 1px solid var(--border);
overflow-y: auto;
}
.tw-task-list-empty {
padding: 24px 16px;
text-align: center;
color: var(--text-muted);
font-size: 13px;
}
.tw-task-detail {
flex: 1;
overflow-y: auto;
padding: 20px;
}
.tw-task-detail-empty {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100%;
text-align: center;
color: var(--text-muted);
gap: 8px;
}
.tw-task-detail-empty p {
margin: 0;
font-size: 14px;
}
.tw-hint {
font-size: 12px;
color: var(--text-muted);
}
/* ============================================
TASK LIST ITEMS
============================================ */
.tw-task-item {
display: flex;
align-items: center;
gap: 10px;
padding: 10px 16px;
border-bottom: 1px solid var(--border);
cursor: pointer;
transition: background 0.15s;
}
.tw-task-item:hover {
background: var(--surface);
}
.tw-task-item.active {
background: var(--accent-light, rgba(132, 214, 105, 0.08));
border-left: 3px solid var(--accent);
}
.tw-task-status-dot {
width: 8px;
height: 8px;
border-radius: 50%;
flex-shrink: 0;
}
.tw-task-status-dot.running {
background: var(--accent);
}
.tw-task-status-dot.pending {
background: var(--warning, #f59e0b);
}
.tw-task-status-dot.completed {
background: var(--success, #3b82f6);
}
.tw-task-status-dot.failed {
background: var(--error, #ef4444);
}
.tw-task-status-dot.paused {
background: var(--text-muted);
}
.tw-task-item-info {
flex: 1;
min-width: 0;
}
.tw-task-item-name {
font-size: 12px;
font-weight: 600;
color: var(--text);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.tw-task-item-type {
font-size: 10px;
color: var(--text-muted);
text-transform: uppercase;
letter-spacing: 0.5px;
}
/* ============================================
AGENT PROFILE VIEW
============================================ */
.tw-agent-profile {
padding: 20px;
}
.tw-agent-header {
display: flex;
align-items: center;
gap: 16px;
margin-bottom: 24px;
padding-bottom: 16px;
border-bottom: 1px solid var(--border);
}
.tw-agent-avatar {
width: 48px;
height: 48px;
border-radius: 50%;
background: var(--accent);
display: flex;
align-items: center;
justify-content: center;
color: var(--bg);
font-size: 20px;
font-weight: 700;
}
.tw-agent-meta {
flex: 1;
}
.tw-agent-title {
font-size: 16px;
font-weight: 700;
color: var(--text);
margin: 0 0 4px 0;
}
.tw-agent-subtitle {
font-size: 12px;
color: var(--text-muted);
}
.tw-section {
margin-bottom: 20px;
}
.tw-section-title {
font-size: 11px;
font-weight: 700;
color: var(--text-muted);
text-transform: uppercase;
letter-spacing: 0.5px;
margin-bottom: 10px;
}
.tw-stat-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
gap: 10px;
}
.tw-stat-card {
padding: 12px;
background: var(--surface);
border-radius: 8px;
border: 1px solid var(--border);
}
.tw-stat-value {
font-size: 18px;
font-weight: 700;
color: var(--text);
}
.tw-stat-label {
font-size: 10px;
color: var(--text-muted);
text-transform: uppercase;
letter-spacing: 0.3px;
margin-top: 2px;
}

View file

@ -0,0 +1,189 @@
<!-- Task Window — Unified Tasks Dashboard (Phase 2) -->
<link rel="stylesheet" href="/suite/tasks/task-window.css" />
<div class="tw-container" id="taskWindow">
<!-- Tab Header -->
<div class="tw-tabs-header" id="twTabsHeader">
<button class="tw-tab active" data-tab="dashboard" id="tabDashboard">
<span class="tw-tab-prefix">//</span> DASHBOARD
</button>
</div>
<!-- Tab Content: Dashboard -->
<div class="tw-tab-content active" id="tabContentDashboard">
<!-- Pipeline Tabs -->
<div class="tw-pipeline-tabs">
<button class="tw-pipeline-tab" data-stage="plan">// PLAN</button>
<button class="tw-pipeline-tab active" data-stage="build">// BUILD</button>
<button class="tw-pipeline-tab" data-stage="review">// REVIEW</button>
<button class="tw-pipeline-tab" data-stage="deploy">// DEPLOY</button>
<button class="tw-pipeline-tab" data-stage="monitor">// MONITOR</button>
</div>
<!-- Filter Chips -->
<div class="tw-filters">
<button class="tw-filter-chip active" data-filter="all">
<span class="tw-filter-icon"></span> All
</button>
<button class="tw-filter-chip" data-filter="active">
<span class="tw-filter-icon"></span> Active
</button>
<button class="tw-filter-chip" data-filter="completed">
<span class="tw-filter-icon"></span> Complete
</button>
<button class="tw-filter-chip" data-filter="paused">
<span class="tw-filter-icon"></span> Paused
</button>
</div>
<!-- Intent Bar -->
<div class="tw-intent-bar">
<input type="text" class="tw-intent-input" id="intentInput"
placeholder="What would you like to do? e.g. 'create a CRM'" />
<button class="tw-intent-run" id="intentRunBtn" type="button">RUN</button>
</div>
<!-- Tasks List + Detail split -->
<div class="tw-split-view">
<div class="tw-task-list" id="taskList" hx-get="/api/ui/tasks?filter=all" hx-trigger="load"
hx-swap="innerHTML">
<div class="tw-task-list-empty">Loading tasks...</div>
</div>
<div class="tw-task-detail" id="taskDetail">
<div class="tw-task-detail-empty">
<p>Select a task</p>
<p class="tw-hint">Click on a task from the list to view details.</p>
<p class="tw-hint">Bot Database: All apps share the same database tables.</p>
</div>
</div>
</div>
</div>
<!-- Tab Content: Agent Profile (dynamically created) -->
<div class="tw-tab-content" id="tabContentAgent" style="display:none;">
<div class="tw-agent-profile" id="agentProfileContent">
<!-- Loaded via HTMX when an agent tab is clicked -->
</div>
</div>
</div>
<script>
(function () {
"use strict";
function initTaskWindow() {
setupTabs();
setupPipelineTabs();
setupFilters();
setupIntentBar();
}
function setupTabs() {
var tabsHeader = document.getElementById("twTabsHeader");
if (!tabsHeader) return;
tabsHeader.addEventListener("click", function (e) {
var tab = e.target.closest(".tw-tab");
if (!tab) return;
var allTabs = tabsHeader.querySelectorAll(".tw-tab");
allTabs.forEach(function (t) { t.classList.remove("active"); });
tab.classList.add("active");
var tabId = tab.getAttribute("data-tab");
var allContent = document.querySelectorAll(".tw-tab-content");
allContent.forEach(function (c) {
c.style.display = "none";
c.classList.remove("active");
});
if (tabId === "dashboard") {
var dash = document.getElementById("tabContentDashboard");
if (dash) { dash.style.display = ""; dash.classList.add("active"); }
} else {
var agent = document.getElementById("tabContentAgent");
if (agent) { agent.style.display = ""; agent.classList.add("active"); }
}
});
}
function setupPipelineTabs() {
var container = document.querySelector(".tw-pipeline-tabs");
if (!container) return;
container.addEventListener("click", function (e) {
var tab = e.target.closest(".tw-pipeline-tab");
if (!tab) return;
container.querySelectorAll(".tw-pipeline-tab").forEach(function (t) {
t.classList.remove("active");
});
tab.classList.add("active");
});
}
function setupFilters() {
var container = document.querySelector(".tw-filters");
if (!container) return;
container.addEventListener("click", function (e) {
var chip = e.target.closest(".tw-filter-chip");
if (!chip) return;
container.querySelectorAll(".tw-filter-chip").forEach(function (c) {
c.classList.remove("active");
});
chip.classList.add("active");
});
}
function setupIntentBar() {
var input = document.getElementById("intentInput");
var runBtn = document.getElementById("intentRunBtn");
if (!input || !runBtn) return;
function submitIntent() {
var text = input.value.trim();
if (!text) return;
runBtn.disabled = true;
runBtn.textContent = "...";
fetch("/api/autotask/classify", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ intent: text })
})
.then(function (r) { return r.json(); })
.then(function (data) {
input.value = "";
runBtn.disabled = false;
runBtn.textContent = "RUN";
if (window.htmx) {
htmx.trigger("#taskList", "load");
}
})
.catch(function (err) {
console.error("Intent submission failed:", err);
runBtn.disabled = false;
runBtn.textContent = "RUN";
});
}
runBtn.addEventListener("click", submitIntent);
input.addEventListener("keydown", function (e) {
if (e.key === "Enter") {
e.preventDefault();
submitIntent();
}
});
}
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", initTaskWindow);
} else {
initTaskWindow();
}
})();
</script>

File diff suppressed because it is too large Load diff

View file

@ -108,19 +108,19 @@
.taskmd-status-badge.status-running {
background: var(--primary, #c5f82a);
color: #000;
color: var(--bg, #000);
}
.taskmd-status-badge.status-completed,
.taskmd-status-badge.status-complete {
background: var(--success, #22c55e);
color: #fff;
background: var(--success, var(--success, #22c55e));
color: var(--text, #fff);
}
.taskmd-status-badge.status-error,
.taskmd-status-badge.status-failed {
background: var(--error, #ef4444);
color: #fff;
background: var(--error, var(--error, #ef4444));
color: var(--text, #fff);
}
.taskmd-status-badge.status-pending {
@ -430,7 +430,7 @@
}
.tree-section-dot.failed {
background: var(--error, #ef4444);
background: var(--error, var(--error, #ef4444));
}
.tree-status.completed {
@ -442,7 +442,7 @@
}
.tree-status.failed {
color: var(--error, #ef4444);
color: var(--error, var(--error, #ef4444));
}
/* Tree Children - visible when expanded (default state) */
@ -525,7 +525,7 @@
}
.tree-item-dot.failed {
background: var(--error, #ef4444);
background: var(--error, var(--error, #ef4444));
}
@keyframes dot-pulse {
@ -567,7 +567,7 @@
}
.tree-item-check.completed {
color: var(--success, #22c55e);
color: var(--success, var(--success, #22c55e));
}
.tree-item-check.running {
@ -584,7 +584,7 @@
}
.tree-item.completed .tree-item-dot {
background: var(--success, #22c55e);
background: var(--success, var(--success, #22c55e));
}
/* Parent section completed when all children done */
@ -757,8 +757,8 @@
}
.taskmd-terminal-output .markdown-content pre {
background: #151515;
border: 1px solid #252525;
background: var(--surface-hover, #151515);
border: 1px solid var(--border-hover, #252525);
border-radius: 6px;
padding: 12px 16px;
margin: 12px 0;
@ -769,7 +769,7 @@
}
.taskmd-terminal-output .markdown-content code {
background: #1a1a1a;
background: var(--surface-active, #1a1a1a);
padding: 2px 6px;
border-radius: 4px;
font-family: "JetBrains Mono", "Fira Code", monospace;
@ -780,7 +780,7 @@
.taskmd-terminal-output .markdown-content pre code {
background: transparent;
padding: 0;
color: #aaa;
color: var(--text-secondary, #aaa);
}
.taskmd-terminal-output .markdown-content blockquote {
@ -799,13 +799,13 @@
.taskmd-terminal-output .markdown-content th,
.taskmd-terminal-output .markdown-content td {
border: 1px solid #252525;
border: 1px solid var(--border-hover, #252525);
padding: 8px 12px;
text-align: left;
}
.taskmd-terminal-output .markdown-content th {
background: #151515;
background: var(--surface-hover, #151515);
font-weight: 600;
color: var(--text, #fff);
}
@ -826,11 +826,11 @@
}
.taskmd-terminal-output .terminal-line.success {
color: var(--success, #22c55e);
color: var(--success, var(--success, #22c55e));
}
.taskmd-terminal-output .terminal-line.error {
color: var(--error, #ef4444);
color: var(--error, var(--error, #ef4444));
}
.taskmd-terminal-output .terminal-line.progress {
@ -841,7 +841,7 @@
/* Markdown-like header styling */
.taskmd-terminal-output .terminal-line:has-text("##"),
.taskmd-terminal-output .terminal-line[data-type="header"] {
color: #fff;
color: var(--text, #fff);
font-weight: 600;
margin-top: 12px;
margin-bottom: 4px;
@ -850,13 +850,13 @@
/* Code block styling */
.taskmd-terminal-output .terminal-code {
background: #151515;
border: 1px solid #252525;
background: var(--surface-hover, #151515);
border: 1px solid var(--border-hover, #252525);
border-radius: 6px;
padding: 12px 16px;
margin: 8px 0;
font-size: 12px;
color: #aaa;
color: var(--text-secondary, #aaa);
overflow-x: auto;
white-space: pre-wrap;
word-break: break-word;
@ -869,28 +869,28 @@
/* List items */
.taskmd-terminal-output .terminal-line.info {
color: #888;
color: var(--text-muted, #888);
}
/* Bold text in terminal (markdown **text**) */
.taskmd-terminal-output strong,
.taskmd-terminal-output b {
color: #fff;
color: var(--text, #fff);
font-weight: 600;
}
/* Inline code (markdown `code`) */
.taskmd-terminal-output code {
background: #1a1a1a;
background: var(--surface-active, #1a1a1a);
padding: 2px 6px;
border-radius: 3px;
font-size: 12px;
color: #c5f82a;
color: var(--accent, #c5f82a);
}
/* Timestamp styling */
.taskmd-terminal-output .terminal-timestamp {
color: #444;
color: var(--text-tertiary, #444);
font-size: 11px;
margin-right: 8px;
}
@ -950,7 +950,7 @@
}
.taskmd-terminal-output .terminal-inline-code {
background: #1a1a1a;
background: var(--surface-active, #1a1a1a);
padding: 2px 6px;
border-radius: 4px;
font-family: "JetBrains Mono", "Fira Code", monospace;
@ -973,7 +973,7 @@
}
.taskmd-terminal-output .check-mark {
color: var(--success, #22c55e);
color: var(--success, var(--success, #22c55e));
font-weight: 600;
}
@ -1016,17 +1016,17 @@
}
.taskmd-actions .btn-open-app {
background: var(--success, #22c55e);
color: #fff;
background: var(--success, var(--success, #22c55e));
color: var(--text, #fff);
font-weight: 600;
padding: 8px 18px;
font-size: 13px;
box-shadow: 0 2px 8px rgba(34, 197, 94, 0.3);
box-shadow: 0 2px 8px var(--success-light, rgba(34, 197, 94, 0.3));
}
.taskmd-actions .btn-open-app:hover {
background: #16a34a;
box-shadow: 0 4px 12px rgba(34, 197, 94, 0.4);
background: var(--success-hover, #16a34a);
box-shadow: 0 4px 12px var(--success-light, rgba(34, 197, 94, 0.4));
transform: translateY(-1px);
}
@ -1039,15 +1039,15 @@
}
.taskmd-actions .btn-cancel:hover {
border-color: var(--error, #ef4444);
color: var(--error, #ef4444);
border-color: var(--error, var(--error, #ef4444));
color: var(--error, var(--error, #ef4444));
}
/* Empty State */
.progress-empty {
padding: 40px 24px;
text-align: center;
color: #555;
color: var(--text-muted, #555);
font-size: 14px;
}
@ -1057,19 +1057,19 @@
align-items: center;
gap: 12px;
padding: 16px 24px;
background: rgba(239, 68, 68, 0.1);
border: 1px solid #ef4444;
background: var(--error-light, rgba(239, 68, 68, 0.1));
border: 1px solid var(--error, #ef4444);
border-radius: 6px;
margin: 16px 24px;
}
.error-alert .error-icon {
color: #ef4444;
color: var(--error, #ef4444);
font-size: 18px;
}
.error-alert .error-text {
color: #ef4444;
color: var(--error, #ef4444);
font-size: 14px;
}
@ -1117,14 +1117,14 @@
left: 20px;
right: 70px;
height: 3px;
background: #1a1a1a;
background: var(--surface-active, #1a1a1a);
border-radius: 2px;
}
.tree-progress-bar {
flex: 1;
height: 3px;
background: #c5f82a;
background: var(--accent, #c5f82a);
border-radius: 2px;
transition: width 0.3s ease-out;
position: relative;
@ -1134,7 +1134,7 @@
.tree-progress-percent {
font-size: 11px;
font-weight: 600;
color: #c5f82a;
color: var(--accent, #c5f82a);
min-width: 36px;
text-align: right;
}

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -162,22 +162,22 @@ body:has(.tasks-app) {
justify-content: center;
gap: 8px;
padding: 14px 28px;
background: #22c55e !important;
background: var(--success, #22c55e) !important;
border: none;
border-radius: 10px;
color: #fff !important;
color: var(--text, #fff) !important;
font-family: inherit;
font-size: 15px;
font-weight: 700;
cursor: pointer;
transition: all 0.2s ease;
white-space: nowrap;
box-shadow: 0 4px 12px rgba(34, 197, 94, 0.4);
box-shadow: 0 4px 12px var(--success-light, rgba(34, 197, 94, 0.4));
}
.btn-create-run:hover {
background: #16a34a !important;
box-shadow: 0 6px 20px rgba(34, 197, 94, 0.5);
background: var(--success-hover, #16a34a) !important;
box-shadow: 0 6px 20px var(--success-light, rgba(34, 197, 94, 0.5));
transform: translateY(-1px);
}
@ -190,7 +190,7 @@ body:has(.tasks-app) {
width: 16px;
height: 16px;
border: 2px solid rgba(0, 0, 0, 0.3);
border-top-color: #000;
border-top-color: var(--bg, #000);
border-radius: 50%;
animation: spin 0.8s linear infinite;
}
@ -234,33 +234,33 @@ body:has(.tasks-app) {
}
.intent-result .result-success {
color: var(--success, #22c55e);
color: var(--success, var(--success, #22c55e));
}
.intent-result .result-error {
color: var(--error, #ef4444);
color: var(--error, var(--error, #ef4444));
}
.intent-result .intent-success {
color: var(--success, #22c55e);
color: var(--success, var(--success, #22c55e));
font-size: 14px;
display: inline-flex;
align-items: center;
gap: 6px;
padding: 8px 12px;
background: rgba(34, 197, 94, 0.1);
background: var(--success-light, rgba(34, 197, 94, 0.1));
border-radius: 6px;
animation: fadeIn 0.3s ease;
}
.intent-result .intent-error {
color: var(--error, #ef4444);
color: var(--error, var(--error, #ef4444));
font-size: 14px;
display: inline-flex;
align-items: center;
gap: 6px;
padding: 8px 12px;
background: rgba(239, 68, 68, 0.1);
background: var(--error-light, rgba(239, 68, 68, 0.1));
border-radius: 6px;
}
@ -449,19 +449,19 @@ body:has(.tasks-app) {
}
.task-card.status-awaiting::before {
background: var(--warning, #f59e0b);
background: var(--warning, var(--warning, #f59e0b));
}
.task-card.status-paused::before {
background: var(--info, #8b5cf6);
background: var(--info, var(--info, #8b5cf6));
}
.task-card.status-complete::before {
background: var(--success, #22c55e);
background: var(--success, var(--success, #22c55e));
}
.task-card.status-error::before {
background: var(--error, #ef4444);
background: var(--error, var(--error, #ef4444));
}
.task-card.status-pending::before {
@ -518,23 +518,23 @@ body:has(.tasks-app) {
}
.task-card-status.status-complete {
background: rgba(34, 197, 94, 0.15);
color: var(--success, #22c55e);
background: var(--success-light, rgba(34, 197, 94, 0.15));
color: var(--success, var(--success, #22c55e));
}
.task-card-status.status-error {
background: rgba(239, 68, 68, 0.15);
color: var(--error, #ef4444);
background: var(--error-light, rgba(239, 68, 68, 0.15));
color: var(--error, var(--error, #ef4444));
}
.task-card-status.status-awaiting {
background: rgba(245, 158, 11, 0.15);
color: var(--warning, #f59e0b);
background: var(--warning-light, rgba(245, 158, 11, 0.15));
color: var(--warning, var(--warning, #f59e0b));
}
.task-card-status.status-paused {
background: rgba(139, 92, 246, 0.15);
color: var(--info, #8b5cf6);
background: var(--info-light, rgba(139, 92, 246, 0.15));
color: var(--info, var(--info, #8b5cf6));
}
.task-card-body {
@ -551,8 +551,8 @@ body:has(.tasks-app) {
.priority-badge.priority-high,
.priority-badge.priority-urgent {
background: rgba(239, 68, 68, 0.15);
color: var(--error, #ef4444);
background: var(--error-light, rgba(239, 68, 68, 0.15));
color: var(--error, var(--error, #ef4444));
}
.priority-badge.priority-medium {
@ -561,8 +561,8 @@ body:has(.tasks-app) {
}
.priority-badge.priority-low {
background: rgba(34, 197, 94, 0.15);
color: var(--success, #22c55e);
background: var(--success-light, rgba(34, 197, 94, 0.15));
color: var(--success, var(--success, #22c55e));
}
.task-card-footer {
@ -611,7 +611,7 @@ body:has(.tasks-app) {
}
.task-card.awaiting .progress-fill {
background: var(--warning, #f59e0b);
background: var(--warning, var(--warning, #f59e0b));
}
.progress-info {
@ -637,7 +637,7 @@ body:has(.tasks-app) {
}
.task-card.awaiting .progress-percent {
color: var(--warning, #f59e0b);
color: var(--warning, var(--warning, #f59e0b));
background: transparent;
}
@ -667,15 +667,15 @@ body:has(.tasks-app) {
}
.status-value.status-awaiting {
color: var(--warning, #f59e0b);
color: var(--warning, var(--warning, #f59e0b));
}
.status-value.status-paused {
color: var(--info, #8b5cf6);
color: var(--info, var(--info, #8b5cf6));
}
.status-value.status-complete {
color: var(--success, #22c55e);
color: var(--success, var(--success, #22c55e));
}
/* Task Card Detail */
@ -784,14 +784,14 @@ body:has(.tasks-app) {
.task-status.task-status-completed,
.task-status.task-status-done {
background: rgba(34, 197, 94, 0.15);
color: var(--success, #22c55e);
background: var(--success-light, rgba(34, 197, 94, 0.15));
color: var(--success, var(--success, #22c55e));
}
.task-status.task-status-error,
.task-status.task-status-failed {
background: rgba(239, 68, 68, 0.15);
color: var(--error, #ef4444);
background: var(--error-light, rgba(239, 68, 68, 0.15));
color: var(--error, var(--error, #ef4444));
}
.task-detail-meta {
@ -872,13 +872,13 @@ body:has(.tasks-app) {
}
.task-detail-section.error-section {
background: rgba(239, 68, 68, 0.1);
border: 1px solid rgba(239, 68, 68, 0.2);
background: var(--error-light, rgba(239, 68, 68, 0.1));
border: 1px solid var(--error-light, rgba(239, 68, 68, 0.2));
}
.task-detail-section .error-text {
font-size: 13px;
color: var(--error, #ef4444);
color: var(--error, var(--error, #ef4444));
line-height: 1.5;
margin: 0;
font-family: monospace;
@ -909,12 +909,12 @@ body:has(.tasks-app) {
.btn-review-decision {
background: var(--primary, #c5f82a);
border-color: var(--primary, #c5f82a);
color: #000;
color: var(--bg, #000);
}
.btn-review-decision:hover {
background: #d4ff4a;
color: #000;
color: var(--bg, #000);
}
/* =============================================================================
@ -994,8 +994,8 @@ body:has(.tasks-app) {
}
.status-badge.awaiting {
background: rgba(245, 158, 11, 0.15);
color: var(--warning, #f59e0b);
background: var(--warning-light, rgba(245, 158, 11, 0.15));
color: var(--warning, var(--warning, #f59e0b));
}
.status-badge.active {
@ -1012,7 +1012,7 @@ body:has(.tasks-app) {
background: var(--primary, #c5f82a);
border: none;
border-radius: 6px;
color: #000;
color: var(--bg, #000);
font-size: 14px;
cursor: pointer;
transition: all 0.2s ease;
@ -1036,8 +1036,8 @@ body:has(.tasks-app) {
/* Decision Alert */
.decision-alert {
margin: 20px 24px;
background: rgba(245, 158, 11, 0.08);
border: 1px solid rgba(245, 158, 11, 0.25);
background: var(--warning-light, rgba(245, 158, 11, 0.08));
border: 1px solid var(--warning-light, rgba(245, 158, 11, 0.25));
border-radius: 16px;
padding: 20px;
}
@ -1059,7 +1059,7 @@ body:has(.tasks-app) {
.decision-title {
font-size: 14px;
font-weight: 600;
color: var(--warning, #f59e0b);
color: var(--warning, var(--warning, #f59e0b));
margin: 0 0 4px 0;
text-transform: uppercase;
letter-spacing: 0.5px;
@ -1095,7 +1095,7 @@ body:has(.tasks-app) {
background: var(--primary, #c5f82a);
border: none;
border-radius: 6px;
color: #000;
color: var(--bg, #000);
font-family: inherit;
font-size: 14px;
font-weight: 600;
@ -1278,13 +1278,13 @@ body:has(.tasks-app) {
}
.log-step.completed {
background: rgba(34, 197, 94, 0.15);
color: var(--success, #22c55e);
background: var(--success-light, rgba(34, 197, 94, 0.15));
color: var(--success, var(--success, #22c55e));
}
.log-status {
font-size: 12px;
color: var(--success, #22c55e);
color: var(--success, var(--success, #22c55e));
}
.log-subitems {
@ -1307,7 +1307,7 @@ body:has(.tasks-app) {
}
.log-dot.completed {
background: var(--success, #22c55e);
background: var(--success, var(--success, #22c55e));
}
.log-subname {
@ -1669,11 +1669,11 @@ body:has(.tasks-app) {
}
.toast.success {
border-color: var(--success, #22c55e);
border-color: var(--success, var(--success, #22c55e));
}
.toast.error {
border-color: var(--error, #ef4444);
border-color: var(--error, var(--error, #ef4444));
}
@keyframes slideIn {
@ -1896,12 +1896,12 @@ body:has(.tasks-app) {
}
.floating-progress-title .progress-dot.completed {
background: var(--success, #22c55e);
background: var(--success, var(--success, #22c55e));
animation: none;
}
.floating-progress-title .progress-dot.error {
background: var(--error, #ef4444);
background: var(--error, var(--error, #ef4444));
animation: none;
}
@ -2079,7 +2079,7 @@ body:has(.tasks-app) {
}
.metric-value.phase-analyzing {
color: var(--warning, #f59e0b);
color: var(--warning, var(--warning, #f59e0b));
}
.metric-value.phase-parsing {
@ -2087,7 +2087,7 @@ body:has(.tasks-app) {
}
.metric-value.phase-database {
color: var(--accent, #8b5cf6);
color: var(--accent, var(--info, #8b5cf6));
}
.metric-value.phase-writing {
@ -2099,19 +2099,19 @@ body:has(.tasks-app) {
}
.metric-value.phase-tools {
color: var(--warning, #f59e0b);
color: var(--warning, var(--warning, #f59e0b));
}
.metric-value.phase-schedulers {
color: var(--accent, #8b5cf6);
color: var(--accent, var(--info, #8b5cf6));
}
.metric-value.phase-syncing {
color: var(--success, #22c55e);
color: var(--success, var(--success, #22c55e));
}
.metric-value.phase-completed {
color: var(--success, #22c55e);
color: var(--success, var(--success, #22c55e));
}
/* =============================================================================
@ -2154,7 +2154,7 @@ body:has(.tasks-app) {
}
.terminal-status.completed {
color: var(--success, #22c55e);
color: var(--success, var(--success, #22c55e));
animation: none;
}
@ -2328,23 +2328,23 @@ body:has(.tasks-app) {
.status-badge-rich.status-running,
.status-badge-rich.status-pending {
background: var(--primary, #c5f82a);
color: #000;
color: var(--bg, #000);
}
.status-badge-rich.status-completed {
background: var(--success, #22c55e);
color: #fff;
background: var(--success, var(--success, #22c55e));
color: var(--text, #fff);
}
.status-badge-rich.status-error,
.status-badge-rich.status-failed {
background: var(--error, #ef4444);
color: #fff;
background: var(--error, var(--error, #ef4444));
color: var(--text, #fff);
}
.status-badge-rich.status-paused {
background: var(--warning, #f59e0b);
color: #000;
background: var(--warning, var(--warning, #f59e0b));
color: var(--bg, #000);
}
/* Section Boxes */
@ -2393,11 +2393,11 @@ body:has(.tasks-app) {
}
.status-dot.status-completed {
background: var(--success, #22c55e);
background: var(--success, var(--success, #22c55e));
}
.status-dot.status-error {
background: var(--error, #ef4444);
background: var(--error, var(--error, #ef4444));
}
@keyframes pulse-status-dot {
@ -2432,19 +2432,19 @@ body:has(.tasks-app) {
align-items: center;
gap: 10px;
padding: 14px 18px;
background: rgba(239, 68, 68, 0.15);
border: 2px solid var(--error, #ef4444);
background: var(--error-light, rgba(239, 68, 68, 0.15));
border: 2px solid var(--error, var(--error, #ef4444));
border-radius: 10px;
margin-bottom: 16px;
}
.error-icon {
color: var(--error, #ef4444);
color: var(--error, var(--error, #ef4444));
font-size: 1.1rem;
}
.error-text {
color: var(--error, #ef4444);
color: var(--error, var(--error, #ef4444));
font-size: 0.85rem;
}
@ -2573,7 +2573,7 @@ body:has(.tasks-app) {
.log-step-badge {
padding: 4px 12px;
background: var(--primary, #c5f82a);
color: #000;
color: var(--bg, #000);
font-size: 0.75rem;
font-weight: 700;
border-radius: 6px;
@ -2589,8 +2589,8 @@ body:has(.tasks-app) {
}
.log-status-badge.completed {
background: rgba(34, 197, 94, 0.15);
color: var(--success, #22c55e);
background: var(--success-light, rgba(34, 197, 94, 0.15));
color: var(--success, var(--success, #22c55e));
}
.log-group-items {
@ -2619,7 +2619,7 @@ body:has(.tasks-app) {
}
.log-dot.completed {
background: var(--success, #22c55e);
background: var(--success, var(--success, #22c55e));
}
.log-dot.running {
@ -2628,7 +2628,7 @@ body:has(.tasks-app) {
}
.log-dot.failed {
background: var(--error, #ef4444);
background: var(--error, var(--error, #ef4444));
}
.log-dot.pending {
@ -2656,7 +2656,7 @@ body:has(.tasks-app) {
.log-item-status {
font-size: 0.75rem;
color: var(--success, #22c55e);
color: var(--success, var(--success, #22c55e));
}
.log-subitem {
@ -2684,7 +2684,7 @@ body:has(.tasks-app) {
}
.log-subdot.failed {
background: var(--error, #ef4444);
background: var(--error, var(--error, #ef4444));
}
.log-subdot.pending {
@ -2769,11 +2769,11 @@ body:has(.tasks-app) {
}
.terminal-line.error {
color: var(--error, #ef4444);
color: var(--error, var(--error, #ef4444));
}
.terminal-line.success {
color: var(--success, #22c55e);
color: var(--success, var(--success, #22c55e));
}
.terminal-line.llm-stream {
@ -2850,41 +2850,41 @@ body:has(.tasks-app) {
}
.btn-action-rich.btn-pause {
border-color: var(--warning, #f59e0b);
border-color: var(--warning, var(--warning, #f59e0b));
border-width: 2px;
color: var(--warning, #f59e0b);
background: rgba(245, 158, 11, 0.08);
color: var(--warning, var(--warning, #f59e0b));
background: var(--warning-light, rgba(245, 158, 11, 0.08));
}
.btn-action-rich.btn-pause:hover {
background: rgba(245, 158, 11, 0.2);
box-shadow: 0 0 12px rgba(245, 158, 11, 0.3);
background: var(--warning-light, rgba(245, 158, 11, 0.2));
box-shadow: 0 0 12px var(--warning-light, rgba(245, 158, 11, 0.3));
}
.btn-action-rich.btn-cancel {
border-color: var(--error, #ef4444);
border-color: var(--error, var(--error, #ef4444));
border-width: 2px;
color: var(--error, #ef4444);
background: rgba(239, 68, 68, 0.08);
color: var(--error, var(--error, #ef4444));
background: var(--error-light, rgba(239, 68, 68, 0.08));
}
.btn-action-rich.btn-cancel:hover {
background: rgba(239, 68, 68, 0.2);
box-shadow: 0 0 12px rgba(239, 68, 68, 0.3);
background: var(--error-light, rgba(239, 68, 68, 0.2));
box-shadow: 0 0 12px var(--error-light, rgba(239, 68, 68, 0.3));
}
.btn-action-rich.btn-open-app {
border-color: var(--success, #22c55e);
border-color: var(--success, var(--success, #22c55e));
border-width: 2px;
color: var(--success, #22c55e);
background: rgba(34, 197, 94, 0.1);
color: var(--success, var(--success, #22c55e));
background: var(--success-light, rgba(34, 197, 94, 0.1));
text-decoration: none;
}
.btn-action-rich.btn-open-app:hover {
background: rgba(34, 197, 94, 0.25);
box-shadow: 0 0 12px rgba(34, 197, 94, 0.4);
color: #fff;
background: var(--success-light, rgba(34, 197, 94, 0.25));
box-shadow: 0 0 12px var(--success-light, rgba(34, 197, 94, 0.4));
color: var(--text, #fff);
}
/* =============================================================================
@ -2915,8 +2915,8 @@ body:has(.tasks-app) {
}
.log-item-done {
background: rgba(34, 197, 94, 0.08);
border-left: 3px solid var(--success, #22c55e);
background: var(--success-light, rgba(34, 197, 94, 0.08));
border-left: 3px solid var(--success, var(--success, #22c55e));
}
.log-item-current {
@ -2930,7 +2930,7 @@ body:has(.tasks-app) {
}
.log-check {
color: var(--success, #22c55e);
color: var(--success, var(--success, #22c55e));
font-weight: bold;
}
@ -2976,19 +2976,19 @@ body:has(.tasks-app) {
align-items: center;
gap: 10px;
padding: 10px 14px;
background: rgba(239, 68, 68, 0.1);
border: 1px solid var(--error, #ef4444);
background: var(--error-light, rgba(239, 68, 68, 0.1));
border: 1px solid var(--error, var(--error, #ef4444));
border-radius: 6px;
margin-top: 10px;
}
.summary-alert .alert-icon {
color: var(--error, #ef4444);
color: var(--error, var(--error, #ef4444));
}
.summary-alert .alert-text {
font-size: 0.8rem;
color: var(--error, #ef4444);
color: var(--error, var(--error, #ef4444));
}
.progress-summary-content {
@ -3075,7 +3075,7 @@ body:has(.tasks-app) {
}
.section-indicator.failed {
background: var(--error, #ef4444);
background: var(--error, var(--error, #ef4444));
}
.section-name {
@ -3244,7 +3244,7 @@ body:has(.tasks-app) {
}
.item-check.completed {
color: var(--success, #22c55e);
color: var(--success, var(--success, #22c55e));
}
.item-check.running {
@ -3444,7 +3444,7 @@ body:has(.tasks-app) {
}
.task-card.completed .task-status-badge {
background: var(--success, #22c55e);
background: var(--success, var(--success, #22c55e));
}
/* Status section in detail panel */

View file

@ -1,4 +1,305 @@
<div class="modal-footer">
<link rel="stylesheet" href="/suite/tasks/tasks-scoped.css" />
<script src="/suite/tasks/tasks-scoped.js"></script>
<!-- =============================================================================
TASKS APP - Autonomous Task Management
Respects Theme Manager - No hardcoded theme
============================================================================= -->
<div class="tasks-app">
<!-- Hidden element to load stats on page load -->
<div
hx-get="/api/ui/tasks/stats"
hx-trigger="load, taskCreated from:div"
hx-swap="innerHTML"
style="display: none"
></div>
<!-- Status Filter Pills Row -->
<div class="status-filter-row">
<button
class="filter-pill"
data-filter="complete"
hx-get="/api/ui/tasks?filter=complete"
hx-target="closest .window-body #task-list"
hx-swap="innerHTML"
>
<span class="pill-icon"></span>
<span class="pill-label" data-i18n="tasks-completed">Complete</span>
<span class="pill-count" id="count-complete">-</span>
</button>
<button
class="filter-pill active"
data-filter="all"
hx-get="/api/ui/tasks?filter=all"
hx-target="closest .window-body #task-list"
hx-swap="innerHTML"
>
<span class="pill-icon">📋</span>
<span class="pill-label" data-i18n="tasks-all">All Tasks</span>
<span class="pill-count" id="count-all">-</span>
</button>
<button
class="filter-pill"
data-filter="active"
hx-get="/api/ui/tasks?filter=active"
hx-target="closest .window-body #task-list"
hx-swap="innerHTML"
>
<span class="pill-icon"></span>
<span class="pill-label" data-i18n="tasks-active"
>Active Intents</span
>
<span class="pill-count" id="count-active">-</span>
</button>
<button
class="filter-pill"
data-filter="awaiting"
hx-get="/api/ui/tasks?filter=awaiting"
hx-target="closest .window-body #task-list"
hx-swap="innerHTML"
>
<span class="pill-icon"></span>
<span class="pill-label" data-i18n="tasks-awaiting"
>Awaiting Decision</span
>
<span class="pill-count" id="count-awaiting">-</span>
</button>
<button
class="filter-pill"
data-filter="paused"
hx-get="/api/ui/tasks?filter=paused"
hx-target="closest .window-body #task-list"
hx-swap="innerHTML"
>
<span class="pill-icon"></span>
<span class="pill-label" data-i18n="tasks-paused">Paused</span>
<span class="pill-count" id="count-paused">-</span>
</button>
<button
class="filter-pill"
data-filter="blocked"
hx-get="/api/ui/tasks?filter=blocked"
hx-target="closest .window-body #task-list"
hx-swap="innerHTML"
>
<span class="pill-icon"></span>
<span class="pill-label" data-i18n="tasks-blocked"
>Blocked/Issues</span
>
<span class="pill-count" id="count-blocked">-</span>
</button>
<div class="time-saved-badge">
<span class="time-label" data-i18n="tasks-time-saved"
>Active Time Saved:</span
>
<span class="time-value" id="time-saved-value">-</span>
</div>
</div>
<!-- Quick Intent Input -->
<div class="quick-intent-bar">
<div class="intent-input-wrapper">
<input
type="text"
id="quick-intent-input"
name="intent"
class="quick-intent-input"
placeholder="What would you like to do? e.g., 'create a CRM app' or 'remind me to call John tomorrow'"
data-i18n-placeholder="tasks-input-placeholder"
autocomplete="off"
/>
<button
id="quick-intent-btn"
class="btn-create-run"
hx-post="/api/ui/autotask/create"
hx-ext="json-enc"
hx-include="#quick-intent-input"
hx-target="closest .window-body #intent-result-hidden"
hx-swap="none"
hx-indicator="#intent-spinner"
hx-timeout="300000"
>
<span class="btn-text">Create & Run</span>
<span class="spinner" id="intent-spinner"></span>
</button>
</div>
<div id="intent-result" class="intent-result"></div>
<div id="intent-result-hidden" style="display: none"></div>
</div>
<!-- Main Two-Column Layout with Splitter -->
<main class="tasks-main">
<!-- Left Panel: Task Cards List -->
<section class="tasks-list-panel">
<div
class="tasks-list-scroll"
id="task-list"
hx-get="/api/ui/tasks?filter=all"
hx-trigger="load, taskCreated from:div throttle:2s"
hx-swap="innerHTML transition:false"
>
<!-- Loading state - replaced by HTMX -->
<div class="loading-state">
<div class="loading-spinner"></div>
<p>Loading tasks...</p>
</div>
</div>
</section>
<!-- Splitter -->
<div class="tasks-splitter" id="tasks-splitter"></div>
<!-- Right Panel: Task Detail -->
<aside class="task-detail-panel" id="task-detail-panel">
<!-- Detail content loaded dynamically -->
<div class="detail-empty" id="detail-empty">
<div class="empty-icon">📋</div>
<h3 class="empty-title">Select a task</h3>
<p class="empty-description">
Click on a task from the list to view details
</p>
<div class="empty-info">
<p>
<strong>Bot Database:</strong> All apps share the same
database tables.
</p>
<p>
<strong>Shared Resources:</strong> Schedulers, tools,
and monitors work across all apps.
</p>
</div>
</div>
<!-- Dynamic detail content -->
<div
id="task-detail-content"
style="display: none"
hx-get=""
hx-trigger="taskSelected from:div"
hx-swap="innerHTML"
>
<!-- Loaded via HTMX when task selected -->
</div>
</aside>
</main>
</div>
<!-- Floating Progress Panel - Shows live task generation progress -->
<div
class="floating-progress-panel"
id="floating-progress"
style="display: none"
>
<div class="floating-progress-header">
<div class="floating-progress-title">
<span class="progress-dot"></span>
<span id="floating-task-name">Processing...</span>
</div>
<div class="floating-progress-actions">
<button class="btn-minimize" onclick="minimizeFloatingProgress()">
</button>
<button class="btn-close-float" onclick="closeFloatingProgress()">
×
</button>
</div>
</div>
<div class="floating-progress-div">
<div class="floating-progress-bar">
<div
class="floating-progress-fill"
id="floating-progress-fill"
style="width: 0%"
></div>
</div>
<div class="floating-progress-info">
<span id="floating-progress-step">Starting...</span>
<span id="floating-progress-percent">0%</span>
</div>
<div class="floating-progress-log" id="floating-progress-log">
<!-- Live log entries appear here -->
</div>
<div class="floating-llm-terminal" id="floating-llm-terminal">
<!-- LLM streaming output appears here -->
</div>
</div>
</div>
<!-- Toast Container -->
<div class="toast-container" id="toast-container"></div>
<!-- New Intent Modal -->
<div class="modal" id="new-intent-modal" style="display: none">
<div class="modal-backdrop" onclick="closeNewIntentModal()"></div>
<div class="modal-content">
<div class="modal-header">
<h3>Create New Intent</h3>
<button class="btn-close" onclick="closeNewIntentModal()">×</button>
</div>
<div class="modal-div">
<form id="new-intent-form">
<div class="form-group">
<label for="intent-text"
>What would you like to accomplish?</label
>
<textarea
id="intent-text"
name="intent"
rows="4"
placeholder="Describe what you want to create or automate..."
></textarea>
</div>
<div class="form-row">
<div class="form-group">
<label for="intent-priority">Priority</label>
<select id="intent-priority" name="priority">
<option value="low">Low</option>
<option value="medium" selected>Medium</option>
<option value="high">High</option>
<option value="urgent">Urgent</option>
</select>
</div>
<div class="form-group">
<label for="intent-mode">Execution Mode</label>
<select id="intent-mode" name="mode">
<option value="auto">Automatic</option>
<option value="supervised">Supervised</option>
<option value="manual">Manual Approval</option>
</select>
</div>
</div>
</form>
</div>
<div class="modal-footer">
<button class="btn-secondary" onclick="closeNewIntentModal()">
Cancel
</button>
<button class="btn-primary" onclick="submitNewIntent()">
Create & Run
</button>
</div>
</div>
</div>
<!-- Decision Modal -->
<div class="modal" id="decision-modal" style="display: none">
<div class="modal-backdrop" onclick="closeDecisionModal()"></div>
<div class="modal-content modal-lg">
<div class="modal-header">
<h3>Make Decision</h3>
<button class="btn-close" onclick="closeDecisionModal()">×</button>
</div>
<div class="modal-div">
<div class="decision-question" id="decision-question">
<h4>Decision Required</h4>
<p>Loading decision details...</p>
</div>
<div class="decision-options" id="decision-options">
<!-- Options loaded dynamically -->
</div>
</div>
<div class="modal-footer">
<button class="btn-secondary" onclick="closeDecisionModal()">
Cancel
</button>
@ -12,4 +313,5 @@
</div>
</div>
<script src="/suite/tasks/tasks.js"></script>
<link rel="stylesheet" href="/suite/tasks/tasks-scoped.css" />
<script src="/suite/tasks/tasks-scoped.js"></script>

View file

@ -0,0 +1,9 @@
<div class="bg-[#1a1a1a] p-6 font-mono text-sm leading-relaxed overflow-y-auto h-full text-gray-300">
<div class="whitespace-pre font-medium text-brand-400">GB OS Terminal v1.0.0</div>
<div class="whitespace-pre font-medium">Type 'help' for a list of commands.</div>
<br>
<div class="mt-1 flex items-center">
<span class="text-brand-400 mr-2">/home/user $</span>
<div class="w-2.5 h-4 bg-brand-400 animate-pulse inline-block"></div>
</div>
</div>

View file

@ -0,0 +1,494 @@
/* Agents & Workspaces Sidebar + Vibe Canvas Styles */
/* ============================================
VIBE CONTAINER
============================================ */
.vibe-container {
display: flex;
flex-direction: column;
height: 100%;
background: var(--bg, #fff);
font-family: 'Fira Code', monospace;
}
/* ============================================
PIPELINE TABS
============================================ */
.vibe-pipeline {
display: flex;
background: var(--surface, #f8f8f8);
border-bottom: 1px solid var(--border, #f0f1f2);
}
.vibe-pipeline-tab {
flex: 1;
height: 34px;
border: none;
border-right: 1px solid var(--border, #f0f1f2);
background: transparent;
font-family: 'Fira Code', monospace;
font-size: 11px;
font-weight: 600;
color: var(--text-muted, #888);
cursor: pointer;
transition: all 0.15s;
}
.vibe-pipeline-tab:last-child {
border-right: none;
}
.vibe-pipeline-tab:hover {
background: var(--bg, #fff);
color: var(--text, #3b3b3b);
}
.vibe-pipeline-tab.active {
background: var(--accent, #84d669);
color: var(--bg, #fff);
}
/* ============================================
VIBE BODY (Canvas + Sidebar)
============================================ */
.vibe-body {
flex: 1;
display: flex;
overflow: hidden;
}
/* ============================================
CANVAS AREA
============================================ */
.vibe-canvas {
flex: 1;
overflow-y: auto;
display: flex;
flex-direction: column;
}
.vibe-canvas-empty {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 12px;
padding: 40px;
text-align: center;
}
.vibe-canvas-icon {
font-size: 48px;
}
.vibe-canvas-empty h3 {
margin: 0;
font-size: 20px;
font-weight: 700;
color: var(--text, #3b3b3b);
}
.vibe-canvas-empty p {
margin: 0;
font-size: 14px;
color: var(--text-muted, #888);
max-width: 400px;
}
.vibe-canvas-prompt {
display: flex;
gap: 8px;
margin-top: 12px;
width: 100%;
max-width: 500px;
}
.vibe-prompt-input {
flex: 1;
padding: 10px 16px;
border: 2px solid var(--border, #e0e0e0);
border-radius: 8px;
font-family: 'Fira Code', monospace;
font-size: 13px;
color: var(--text, #3b3b3b);
outline: none;
transition: border-color 0.2s;
}
.vibe-prompt-input:focus {
border-color: var(--accent, #84d669);
box-shadow: 0 0 0 3px var(--accent-light, rgba(132, 214, 105, 0.15));
}
.vibe-prompt-input::placeholder {
color: var(--text-muted, #bbb);
}
.vibe-prompt-btn {
padding: 10px 24px;
border: none;
border-radius: 8px;
background: var(--accent, #84d669);
color: var(--bg, #fff);
font-family: 'Fira Code', monospace;
font-size: 13px;
font-weight: 700;
cursor: pointer;
transition: all 0.15s;
}
.vibe-prompt-btn:hover {
background: var(--accent-hover, #72c458);
transform: translateY(-1px);
box-shadow: 0 4px 12px var(--accent-light, rgba(132, 214, 105, 0.3));
}
.vibe-prompt-btn:disabled {
opacity: 0.6;
cursor: not-allowed;
transform: none;
}
/* Steps */
.vibe-steps {
padding: 16px;
}
/* Preview */
.vibe-preview {
flex: 1;
display: flex;
flex-direction: column;
border-top: 1px solid var(--border, #f0f1f2);
}
.vibe-preview-header {
display: flex;
align-items: center;
gap: 8px;
padding: 6px 12px;
background: var(--surface, #f8f8f8);
border-bottom: 1px solid var(--border, #f0f1f2);
font-family: 'Fira Code', monospace;
font-size: 11px;
font-weight: 600;
color: var(--text, #3b3b3b);
}
.vibe-preview-url {
flex: 1;
padding: 3px 10px;
background: var(--bg, #fff);
border: 1px solid var(--border, #e0e0e0);
border-radius: 4px;
font-family: 'Fira Code', monospace;
font-size: 11px;
color: var(--text-muted, #666);
}
.vibe-preview-content {
flex: 1;
}
.vibe-preview-content iframe {
width: 100%;
height: 100%;
border: none;
}
/* ============================================
AGENTS & WORKSPACES SIDEBAR
============================================ */
.agents-sidebar {
width: 260px;
min-width: 260px;
background: var(--surface, #f8f8f8);
border-left: 1px solid var(--border, #f0f1f2);
overflow-y: auto;
transition: width 0.25s, min-width 0.25s;
}
.agents-sidebar.collapsed {
width: 0;
min-width: 0;
overflow: hidden;
}
/* Section */
.as-section {
border-bottom: 1px solid var(--border, #f0f1f2);
}
.as-section-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 12px 14px 8px 14px;
}
.as-section-header h3 {
margin: 0;
font-size: 11px;
font-weight: 700;
color: var(--text-muted, #888);
text-transform: uppercase;
letter-spacing: 0.5px;
}
.as-collapse-btn {
width: 20px;
height: 20px;
border: none;
background: transparent;
color: var(--text-muted, #888);
cursor: pointer;
font-size: 10px;
padding: 0;
display: flex;
align-items: center;
justify-content: center;
border-radius: 4px;
transition: all 0.15s;
}
.as-collapse-btn:hover {
background: var(--surface-active, #e0e0e0);
color: var(--text, #3b3b3b);
}
/* ============================================
AGENT CARDS
============================================ */
.as-agent-list {
padding: 0 8px;
}
.as-agent-card {
background: var(--bg, #fff);
border: 1px solid var(--border, #f0f1f2);
border-radius: 8px;
padding: 10px 12px;
margin-bottom: 6px;
cursor: grab;
transition: all 0.15s;
}
.as-agent-card:hover {
border-color: var(--accent, #84d669);
box-shadow: 0 2px 8px var(--accent-light, rgba(132, 214, 105, 0.1));
}
.as-agent-card.dragging {
opacity: 0.5;
transform: scale(0.95);
}
.as-agent-header {
display: flex;
align-items: center;
gap: 6px;
margin-bottom: 6px;
}
.as-status-dot {
width: 8px;
height: 8px;
border-radius: 50%;
flex-shrink: 0;
}
.as-status-dot.green {
background: var(--accent, #84d669);
}
.as-status-dot.yellow {
background: var(--warning, #f59e0b);
}
.as-status-dot.red {
background: var(--error, #ef4444);
}
.as-status-dot.gray {
background: var(--text-muted, #ccc);
}
.as-agent-name {
flex: 1;
font-size: 12px;
font-weight: 600;
color: var(--text, #3b3b3b);
}
.as-drag-handle {
color: var(--text-muted, #ccc);
font-size: 14px;
cursor: grab;
}
.as-agent-body {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 6px;
}
.as-agent-icons {
font-size: 12px;
letter-spacing: 2px;
}
.as-badge {
display: inline-block;
padding: 1px 6px;
border-radius: 3px;
font-size: 9px;
font-weight: 700;
letter-spacing: 0.5px;
text-transform: uppercase;
}
.badge-evolved {
background: var(--accent, #84d669);
color: var(--bg, #fff);
}
.badge-bred {
background: var(--warning, #f59e0b);
color: var(--bg, #fff);
}
.badge-wild {
background: var(--error, #ef4444);
color: var(--bg, #fff);
}
/* Quota Bar */
.as-agent-bar {
height: 3px;
background: var(--surface, #f0f1f2);
border-radius: 2px;
overflow: hidden;
}
.as-bar-fill {
height: 100%;
background: var(--accent, #84d669);
border-radius: 2px;
transition: width 0.3s;
}
.as-bar-fill.bred {
background: var(--warning, #f59e0b);
}
.as-bar-fill.wild {
background: var(--error, #ef4444);
}
/* Create Agent Button */
.as-create-btn {
width: calc(100% - 16px);
margin: 8px 8px;
padding: 8px;
border: 2px dashed var(--border, #e0e0e0);
border-radius: 8px;
background: transparent;
font-family: 'Fira Code', monospace;
font-size: 11px;
font-weight: 500;
color: var(--text-muted, #888);
cursor: pointer;
transition: all 0.15s;
}
.as-create-btn:hover {
border-color: var(--accent, #84d669);
color: var(--accent, #84d669);
background: var(--accent-light, rgba(132, 214, 105, 0.05));
}
/* ============================================
WORKSPACES
============================================ */
.as-workspace-list {
padding: 0 8px 8px 8px;
}
.as-workspace-item {
margin-bottom: 4px;
}
.as-workspace-toggle {
display: flex;
align-items: center;
gap: 6px;
width: 100%;
padding: 8px 10px;
border: none;
border-radius: 6px;
background: transparent;
font-family: 'Fira Code', monospace;
font-size: 12px;
font-weight: 500;
color: var(--text, #3b3b3b);
cursor: pointer;
transition: background 0.15s;
text-align: left;
}
.as-workspace-toggle:hover {
background: var(--bg, #fff);
}
.as-workspace-arrow {
font-size: 8px;
color: var(--text-muted, #888);
width: 8px;
transition: transform 0.15s;
}
.as-workspace-count {
margin-left: auto;
font-size: 10px;
background: var(--surface-active, #e0e0e0);
color: var(--text-muted, #666);
padding: 1px 6px;
border-radius: 8px;
}
.as-workspace-body {
padding: 4px 0 4px 20px;
}
.as-workspace-agent {
padding: 4px 10px;
font-size: 11px;
color: var(--text-muted, #666);
border-left: 2px solid var(--border, #f0f1f2);
}
.as-workspace-dropzone {
margin-top: 4px;
padding: 10px;
border: 2px dashed var(--border, #e0e0e0);
border-radius: 6px;
text-align: center;
font-size: 11px;
color: var(--text-muted, #ccc);
transition: all 0.2s;
}
.as-workspace-dropzone.drag-over {
border-color: var(--accent, #84d669);
background: var(--accent-light, rgba(132, 214, 105, 0.05));
color: var(--accent, #84d669);
}

Binary file not shown.

Binary file not shown.

Binary file not shown.