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
This commit is contained in:
Rodrigo Rodriguez (Pragmatismo) 2026-02-24 19:02:48 -03:00
parent 6afeeb311f
commit 2f53b65aeb
40 changed files with 23421 additions and 6764 deletions

View file

@ -13,7 +13,7 @@ workspace = true
features = ["http-client"] features = ["http-client"]
[features] [features]
default = ["ui-server", "embed-ui", "chat", "drive", "tasks", "admin"] default = ["ui-server", "chat", "drive", "tasks", "admin"]
ui-server = [] ui-server = []
embed-ui = ["rust-embed"] 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-white text-gray-800 font-sans selection:bg-brand-200">
<div class="flex-1 flex overflow-hidden relative">
<!-- LEFT SIDEBAR -->
<aside class="w-14 shrink-0 bg-white border-r border-gray-100 flex flex-col items-center py-6 z-20 relative">
<div class="flex flex-col space-y-8 text-gray-500">
<button class="hover:text-brand-600 transition-colors"><i class="fa-solid fa-chevron-right"></i></button>
<button class="hover:text-brand-600 transition-colors text-xl"><i class="fa-solid fa-house"></i></button>
<button class="hover:text-brand-600 transition-colors text-xl"><i class="fa-solid fa-magnifying-glass"></i></button>
<button class="hover:text-brand-600 transition-colors text-xl"><i class="fa-solid fa-border-all"></i></button>
<button class="hover:text-brand-600 transition-colors text-xl"><i class="fa-regular fa-user"></i></button>
<button class="hover:text-brand-600 transition-colors text-xl"><i class="fa-solid fa-layer-group"></i></button>
<button class="hover:text-brand-600 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-white border-b border-gray-100 flex items-center px-6">
<div class="font-mono text-xs font-semibold tracking-wider flex space-x-3 items-center">
<span class="text-gray-400">// DASHBOARD</span>
<span class="text-gray-300">&gt;</span>
<span class="text-gray-800">// E-COMMERCE APP DEVELOPMENT</span>
</div>
</header>
<!-- STAGE NAVIGATION -->
<nav class="h-12 shrink-0 bg-white/90 backdrop-blur-sm border-b border-gray-100 flex items-center font-mono text-xs font-semibold z-10">
<div class="flex-1 flex justify-center border-r border-gray-100 py-3 hover:bg-gray-50 cursor-pointer text-gray-400 transition-colors">
// PLAN
</div>
<div class="flex-1 flex justify-center border-r border-gray-100 py-3 bg-brand-50 text-brand-600 cursor-pointer border-b-2 border-b-brand-500 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-gray-100 py-3 hover:bg-gray-50 cursor-pointer text-gray-400 transition-colors">
// REVIEW
</div>
<div class="flex-1 flex justify-center border-r border-gray-100 py-3 hover:bg-gray-50 cursor-pointer text-gray-400 transition-colors">
// DEPLOY
</div>
<div class="flex-1 flex justify-center py-3 hover:bg-gray-50 cursor-pointer text-gray-400 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

@ -126,6 +126,7 @@ const ROOT_FILES: &[&str] = &[
"base.html", "base.html",
"base-layout.html", "base-layout.html",
"base-layout.css", "base-layout.css",
"desktop.html",
"default.gbui", "default.gbui",
"single.gbui", "single.gbui",
]; ];
@ -305,11 +306,11 @@ pub async fn serve_suite(bot_name: Option<String>) -> impl IntoResponse {
let raw_html_res = { let raw_html_res = {
#[cfg(feature = "embed-ui")] #[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()), Some(f) => String::from_utf8(f.data.into_owned()).map_err(|e| e.to_string()),
None => { None => {
let path = get_ui_root().join("suite/index.html"); let path = get_ui_root().join("suite/desktop.html");
log::warn!("Asset 'suite/index.html' not found in embedded binary, falling back to filesystem: {:?}", path); log::warn!("Asset .suite/desktop.html. not found in embedded binary, falling back to filesystem: {:?}", path);
fs::read_to_string(&path).map_err(|e| { fs::read_to_string(&path).map_err(|e| {
format!( format!(
"Asset not found in binary AND failed to read {:?} (CWD: {:?}): {}", "Asset not found in binary AND failed to read {:?} (CWD: {:?}): {}",
@ -323,7 +324,7 @@ pub async fn serve_suite(bot_name: Option<String>) -> impl IntoResponse {
} }
#[cfg(not(feature = "embed-ui"))] #[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| { fs::read_to_string(&path).map_err(|e| {
format!( format!(
"Failed to read {:?} (CWD: {:?}): {}", "Failed to read {:?} (CWD: {:?}): {}",

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 = var WS_BASE_URL =
window.location.protocol === "https:" ? "wss://" : "ws://"; 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 = { var MessageType = {
EXTERNAL: 0, EXTERNAL: 0,
@ -871,10 +871,12 @@
currentUserId + currentUserId +
"&bot_name=" + "&bot_name=" +
currentBotName; currentBotName;
console.log("Connecting WebSocket to:", url);
ws = new WebSocket(url); ws = new WebSocket(url);
ws.onopen = function () { ws.onopen = function () {
console.log("WebSocket connected"); console.log("WebSocket connected to:", url);
reconnectAttempts = 0; reconnectAttempts = 0;
disconnectNotified = false; disconnectNotified = false;
updateConnectionStatus("connected"); updateConnectionStatus("connected");

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

@ -0,0 +1,140 @@
.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;
}
/* Window Manager Core Styles (replacing missing Tailwind classes) */
.window-element {
position: absolute;
width: 700px;
height: 500px;
background-color: white;
border-radius: 8px;
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
display: flex;
flex-direction: column;
border: 1px solid #e5e7eb;
overflow: hidden;
z-index: 100;
}
.window-header {
height: 40px;
background-color: 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 #e5e7eb;
user-select: none;
cursor: move;
}
.window-header .font-mono {
font-family: 'Fira Code', monospace;
font-size: 12px;
font-weight: 700;
color: #16a34a; /* brand-600 */
letter-spacing: 0.025em;
}
.window-header-controls {
display: flex;
gap: 12px;
color: #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: #4b5563;
}
.window-header .btn-close:hover {
color: #ef4444;
}
.window-body {
position: relative;
flex: 1 1 0%;
overflow-y: auto;
background-color: #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: 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: #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: #9ca3af; }
.hover\:text-gray-600:hover { color: #4b5563; }
.hover\:text-red-500:hover { color: #ef4444; }
.relative { position: relative; }
.flex-1 { flex: 1 1 0%; }
.overflow-y-auto { overflow-y: auto; }
.bg-\[\#fafdfa\] { background-color: #fafdfa; }

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.

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

@ -0,0 +1,261 @@
<!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="css/app.css" />
<link rel="stylesheet" href="css/base.css" />
<link rel="stylesheet" href="css/theme-sentient.css" />
<link rel="stylesheet" href="css/desktop.css" />
<!-- Local JS requirements per AGENTS.md / UI.md -->
<script src="js/vendor/htmx.min.js"></script>
<script src="js/window-manager.js"></script>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: 'Fira Code', 'Fira Sans', Arial, sans-serif;
background: white;
overflow: hidden;
height: 100vh;
width: 100vw;
display: flex;
}
/* Core Layout replicating BUILD V3 screenshot styling */
.build-container { width: 100%; height: 100vh; display: flex; }
/* Left Sidebar */
.sidebar { width: 51px; height: 100vh; background: #f8f8f8; border-right: 1px solid #f0f1f2; display: flex; flex-direction: column; z-index: 100; }
.sidebar-item { width: 51px; height: 50px; border-bottom: 1px solid #f0f1f2; display: flex; align-items: center; justify-content: center; cursor: pointer; transition: all 0.15s ease; }
.sidebar-item:hover { background: #ffffff; }
.sidebar-item.active { background: #ffffff; border-left: 3px solid #84d669; }
.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; }
/* Top Navigation Tabs */
.tabs-container { display: flex; flex-direction: column; background: #f8f8f8; border-bottom: 1px solid #f0f1f2; z-index: 100;}
.tabs-row { display: flex; height: 34px; }
.main-tab { height: 34px; min-width: 169px; flex: 1; border-right: 1px solid #f0f1f2; display: flex; align-items: center; padding: 0 18px; cursor: pointer; }
.main-tab:hover { background: #ffffff; }
.main-tab.active { background: #84d669; }
.main-tab.active .main-tab-content { color: white; }
.main-tab-content { display: flex; align-items: center; gap: 4px; font-family: 'Fira Code', monospace; font-size: 14px; font-weight: 500; color: #3b3b3b; }
/* Workspace (Where windows float) */
.workspace { flex: 1; display: flex; flex-direction: column; overflow: hidden; background: white; 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: linear-gradient(135deg, #4ade80 0%, #15803d 100%);
box-shadow: inset 0px 2px 4px rgba(255,255,255,0.4), inset 0px -3px 0 rgba(20,83,45,0.8), 0px 6px 12px rgba(0,0,0,0.15);
display: flex; align-items: center; justify-content: center;
transition: transform 0.15s ease;
}
.desktop-icon:hover .app-icon { transform: scale(1.05); }
.app-icon svg { width: 32px; height: 32px; stroke: white; }
.desktop-icon-label {
font-family: 'Fira Code', monospace; font-size: 12px; font-weight: 600; color: #374151;
background: rgba(255, 255, 255, 0.7); backdrop-filter: blur(4px);
padding: 2px 8px; border-radius: 4px; text-align: center;
}
/* The background abstract pattern from BUILD V3 */
.bg-grid { position: absolute; inset: 0; background-image: linear-gradient(to right, #f0fdf4 1px, transparent 1px), linear-gradient(to bottom, #f0fdf4 1px, transparent 1px); background-size: 40px 40px; z-index: 0; pointer-events: none; }
.bg-svg { position: absolute; top: -10%; left: -10%; width: 120%; height: 120%; z-index: 0; opacity: 0.6; pointer-events: none; }
.bg-svg path { fill: none; stroke: #e6f2eb; stroke-width: 45; stroke-linecap: round; stroke-linejoin: round; }
.bg-svg path.inner { stroke: #f7faf9; stroke-width: 41; }
/* Bottom Taskbar */
.toolbar { height: 50px; background: white; border-top: 1px solid #f0f1f2; 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: #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: #f8f8f8; }
.taskbar-item.active { border-bottom-color: #84d669; background: linear-gradient(to bottom, rgba(132,214,105,0) 50%, rgba(132,214,105,0.1) 100%); }
</style>
</head>
<body>
<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="#3b3b3b" 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="#3b3b3b" 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="#3b3b3b" 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="#3b3b3b" 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="#3b3b3b" 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="#3b3b3b" 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 #f0f1f2; align-items: center; padding: 0 18px; font-family: 'Fira Code', monospace; font-size: 14px; color: #6b7280; gap: 8px;">
<span style="color: #374151;">// DASHBOARD</span>
<span style="color: #9ca3af;">></span>
<span style="color: #374151;">// 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="bg-grid"></div>
<svg class="bg-svg" preserveAspectRatio="xMidYMid slice" viewBox="0 0 1000 600">
<path d="M-50,200 Q200,100 400,250 T800,50 T1100,250" />
<path d="M100,-50 Q250,200 150,450 T400,650" />
<path d="M500,-50 Q450,250 800,350 T750,700" />
<path class="inner" d="M-50,200 Q200,100 400,250 T800,50 T1100,250" />
<path class="inner" d="M100,-50 Q250,200 150,450 T400,650" />
<path class="inner" d="M500,-50 Q450,250 800,350 T750,700" />
</svg>
<div class="panel-section">
<div class="desktop-icons-container">
<div class="desktop-icon" data-app-id="vibe" data-app-title="Mantis (Vibe)" hx-get="/suite/chat/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="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">Mantis</span>
</div>
<div class="desktop-icon" data-app-id="tasks" data-app-title="Tasks" hx-get="/suite/tasks/tasks.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/chat/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">
<div id="clock-time">00:00</div>
<div id="clock-date">01/01/2026</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");
}
});
// 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);
}
}
});
// 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"> <nav class="nav-section">
<div class="nav-item active" <div class="nav-item active"
hx-get="/api/drive/files?path=/" hx-get="/api/drive/files?path=/"
hx-target="#file-grid" hx-target="closest .window-body #file-grid"
hx-swap="innerHTML" hx-swap="innerHTML"
onclick="setActiveNav(this)"> onclick="setActiveNav(this)">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> <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" <div class="nav-item"
hx-get="/api/drive/files?filter=shared" hx-get="/api/drive/files?filter=shared"
hx-target="#file-grid" hx-target="closest .window-body #file-grid"
hx-swap="innerHTML" hx-swap="innerHTML"
onclick="setActiveNav(this)"> onclick="setActiveNav(this)">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> <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" <div class="nav-item"
hx-get="/api/drive/files?filter=recent" hx-get="/api/drive/files?filter=recent"
hx-target="#file-grid" hx-target="closest .window-body #file-grid"
hx-swap="innerHTML" hx-swap="innerHTML"
onclick="setActiveNav(this)"> onclick="setActiveNav(this)">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> <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" <div class="nav-item"
hx-get="/api/drive/files?filter=starred" hx-get="/api/drive/files?filter=starred"
hx-target="#file-grid" hx-target="closest .window-body #file-grid"
hx-swap="innerHTML" hx-swap="innerHTML"
onclick="setActiveNav(this)"> onclick="setActiveNav(this)">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> <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" <div class="nav-item"
hx-get="/api/drive/files?filter=trash" hx-get="/api/drive/files?filter=trash"
hx-target="#file-grid" hx-target="closest .window-body #file-grid"
hx-swap="innerHTML" hx-swap="innerHTML"
onclick="setActiveNav(this)"> onclick="setActiveNav(this)">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> <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" <select class="sort-dropdown" id="sort-dropdown"
hx-get="/api/drive/files" hx-get="/api/drive/files"
hx-target="#file-grid" hx-target="closest .window-body #file-grid"
hx-swap="innerHTML" hx-swap="innerHTML"
hx-include="[name='path']"> hx-include="[name='path']">
<option value="name">Name</option> <option value="name">Name</option>
@ -264,7 +264,7 @@
hx-delete="/api/drive/files" hx-delete="/api/drive/files"
hx-include=".file-checkbox:checked" hx-include=".file-checkbox:checked"
hx-confirm="Move selected items to trash?" hx-confirm="Move selected items to trash?"
hx-target="#file-grid" hx-target="closest .window-body #file-grid"
hx-swap="innerHTML"> hx-swap="innerHTML">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> <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> <polyline points="3 6 5 6 21 6"></polyline>
@ -322,7 +322,7 @@
<div class="upload-zone" id="upload-zone" <div class="upload-zone" id="upload-zone"
hx-post="/api/drive/upload" hx-post="/api/drive/upload"
hx-encoding="multipart/form-data" hx-encoding="multipart/form-data"
hx-target="#file-grid" hx-target="closest .window-body #file-grid"
hx-swap="innerHTML"> hx-swap="innerHTML">
<input type="file" id="file-input" name="files" multiple hidden> <input type="file" id="file-input" name="files" multiple hidden>
<input type="hidden" name="path" id="upload-path" value="/"> <input type="hidden" name="path" id="upload-path" value="/">
@ -352,7 +352,7 @@
<dialog class="modal" id="folder-modal"> <dialog class="modal" id="folder-modal">
<form class="modal-content" <form class="modal-content"
hx-post="/api/drive/folder" hx-post="/api/drive/folder"
hx-target="#file-grid" hx-target="closest .window-body #file-grid"
hx-swap="innerHTML" hx-swap="innerHTML"
hx-on::after-request="document.getElementById('folder-modal').close()"> hx-on::after-request="document.getElementById('folder-modal').close()">
<div class="modal-header"> <div class="modal-header">
@ -418,7 +418,7 @@
<dialog class="modal" id="copy-modal"> <dialog class="modal" id="copy-modal">
<form class="modal-content" <form class="modal-content"
hx-post="/files/copy" hx-post="/files/copy"
hx-target="#file-grid" hx-target="closest .window-body #file-grid"
hx-swap="innerHTML" hx-swap="innerHTML"
hx-on::after-request="document.getElementById('copy-modal').close()"> hx-on::after-request="document.getElementById('copy-modal').close()">
<div class="modal-header"> <div class="modal-header">
@ -453,7 +453,7 @@
<dialog class="modal" id="move-modal"> <dialog class="modal" id="move-modal">
<form class="modal-content" <form class="modal-content"
hx-post="/files/move" hx-post="/files/move"
hx-target="#file-grid" hx-target="closest .window-body #file-grid"
hx-swap="innerHTML" hx-swap="innerHTML"
hx-on::after-request="document.getElementById('move-modal').close()"> hx-on::after-request="document.getElementById('move-modal').close()">
<div class="modal-header"> <div class="modal-header">
@ -562,7 +562,7 @@
</h3> </h3>
<p>Combine multiple documents into one</p> <p>Combine multiple documents into one</p>
<form hx-post="/docs/merge" <form hx-post="/docs/merge"
hx-target="#docs-result" hx-target="closest .window-body #docs-result"
hx-swap="innerHTML" hx-swap="innerHTML"
hx-encoding="multipart/form-data"> hx-encoding="multipart/form-data">
<input type="file" name="files" multiple accept=".pdf,.docx,.doc,.txt" class="form-group" style="margin-bottom: 8px;"> <input type="file" name="files" multiple accept=".pdf,.docx,.doc,.txt" class="form-group" style="margin-bottom: 8px;">
@ -584,7 +584,7 @@
</h3> </h3>
<p>Convert between document formats</p> <p>Convert between document formats</p>
<form hx-post="/docs/convert" <form hx-post="/docs/convert"
hx-target="#docs-result" hx-target="closest .window-body #docs-result"
hx-swap="innerHTML" hx-swap="innerHTML"
hx-encoding="multipart/form-data"> hx-encoding="multipart/form-data">
<input type="file" name="file" accept=".pdf,.docx,.doc,.txt,.md,.html" class="form-group" style="margin-bottom: 8px;"> <input type="file" name="file" accept=".pdf,.docx,.doc,.txt,.md,.html" class="form-group" style="margin-bottom: 8px;">
@ -612,7 +612,7 @@
</h3> </h3>
<p>Populate template with data</p> <p>Populate template with data</p>
<form hx-post="/docs/fill" <form hx-post="/docs/fill"
hx-target="#docs-result" hx-target="closest .window-body #docs-result"
hx-swap="innerHTML" hx-swap="innerHTML"
hx-encoding="multipart/form-data"> hx-encoding="multipart/form-data">
<input type="file" name="template" accept=".docx,.doc" class="form-group" style="margin-bottom: 8px;"> <input type="file" name="template" accept=".docx,.doc" class="form-group" style="margin-bottom: 8px;">
@ -633,7 +633,7 @@
</h3> </h3>
<p>Export document in specified format</p> <p>Export document in specified format</p>
<form hx-post="/docs/export" <form hx-post="/docs/export"
hx-target="#docs-result" hx-target="closest .window-body #docs-result"
hx-swap="innerHTML"> 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;"> <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;"> <select name="format" class="sort-dropdown" style="width: 100%; margin-bottom: 8px;">
@ -657,7 +657,7 @@
</h3> </h3>
<p>Import document from URL or upload</p> <p>Import document from URL or upload</p>
<form hx-post="/docs/import" <form hx-post="/docs/import"
hx-target="#docs-result" hx-target="closest .window-body #docs-result"
hx-swap="innerHTML" hx-swap="innerHTML"
hx-encoding="multipart/form-data"> hx-encoding="multipart/form-data">
<input type="url" name="url" placeholder="Document URL (optional)" class="form-group" style="width: 100%; margin-bottom: 8px;"> <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

@ -59,10 +59,19 @@ const Omnibox = {
this.chatInput = document.getElementById("omniboxChatInput"); this.chatInput = document.getElementById("omniboxChatInput");
this.modeToggle = document.getElementById("omniboxModeToggle"); this.modeToggle = document.getElementById("omniboxModeToggle");
this.bindEvents(); // Only bind events if all required elements exist
if (this.input && this.backdrop) {
this.bindEvents();
}
}, },
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 // Input focus/blur
this.input.addEventListener("focus", () => this.open()); this.input.addEventListener("focus", () => this.open());
this.backdrop.addEventListener("click", () => this.close()); 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/")) { if (window.location.pathname.startsWith("/auth/")) {
console.log("[SPA] Skipping initialization on auth page"); 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") { } else if (document.readyState === "complete") {
setTimeout(initialLoad, 50); setTimeout(initialLoad, 50);
} else { } else {

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,291 @@
if (typeof window.WindowManager === 'undefined') {
class WindowManager {
constructor() {
this.openWindows = [];
this.activeWindowId = null;
this.zIndexCounter = 100;
this.workspace = document.getElementById('desktop-content') || document.body;
this.taskbarApps = document.getElementById('taskbar-apps');
}
open(id, title, htmlContent) {
// 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 = 'absolute w-[700px] h-[500px] bg-white rounded-lg shadow-2xl flex flex-col border border-gray-200 overflow-hidden 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 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">${title}</div>
<div class="flex space-x-3 text-gray-400">
<button class="btn-minimize hover:text-gray-600" onclick="window.WindowManager.toggleMinimize('${id}')"><i class="fa-solid fa-minus"></i></button>
<button class="btn-maximize hover:text-gray-600" onclick="window.WindowManager.toggleMaximize('${id}')"><i class="fa-regular fa-square"></i></button>
<button class="btn-close hover:text-red-500" onclick="window.WindowManager.close('${id}')"><i class="fa-solid fa-xmark"></i></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 = 'h-10 w-12 flex items-center justify-center cursor-pointer bg-brand-50 rounded border-b-2 border-brand-500 transition-all taskbar-icon';
taskbarIcon.onclick = () => this.toggleMinimize(id);
let iconHtml = '<i class="fa-solid fa-window-maximize"></i>';
if (id === 'vibe') iconHtml = '<i class="fa-solid fa-microchip"></i>';
else if (id === 'tasks') iconHtml = '<i class="fa-solid fa-clipboard-list"></i>';
else if (id === 'chat') iconHtml = '<i class="fa-solid fa-comment-dots"></i>';
else if (id === 'terminal') iconHtml = '<i class="fa-solid fa-terminal"></i>';
else if (id === 'drive') iconHtml = '<i class="fa-regular fa-folder-open"></i>';
else if (id === 'editor') iconHtml = '<i class="fa-solid fa-code"></i>';
else if (id === 'browser') iconHtml = '<i class="fa-regular fa-compass"></i>';
else if (id === 'mail') iconHtml = '<i class="fa-regular fa-envelope"></i>';
else if (id === 'settings') iconHtml = '<i class="fa-solid fa-gear"></i>';
taskbarIcon.innerHTML = `
<div class="app-icon w-8 h-8 rounded-md flex items-center justify-center text-white text-xs shadow-sm">
${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('border-brand-500');
icon.classList.add('border-transparent');
});
const activeIcon = document.getElementById(`taskbar-item-${id}`);
if (activeIcon) {
activeIcon.classList.remove('border-transparent');
activeIcon.classList.add('border-brand-500');
}
}
}
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 border 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();
}

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

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,198 @@
<style>
/* CRITICAL: Overriding default Tailwind resets that break the layout */
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: 'Fira Code', 'Fira Sans', Arial, sans-serif !important;
background: white !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: 100vh; display: flex; position: absolute; inset: 0; z-index: 1000; background: white;}
/* Left Sidebar */
.sidebar { width: 51px; height: 100vh; background: #f8f8f8; border-right: 1px solid #f0f1f2; display: flex; flex-direction: column; z-index: 100; }
.sidebar-item { width: 51px; height: 50px; border-bottom: 1px solid #f0f1f2; display: flex; align-items: center; justify-content: center; cursor: pointer; transition: all 0.15s ease; position: relative;}
.sidebar-item:hover { background: #ffffff; }
.sidebar-item.active { background: #ffffff; border-left: 3px solid #84d669; }
.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: #f8f8f8; border-bottom: 1px solid #f0f1f2; z-index: 100;}
.tabs-row { display: flex; height: 34px; }
.main-tab { height: 34px; min-width: 169px; flex: 1; border-right: 1px solid #f0f1f2; display: flex; align-items: center; padding: 0 18px; cursor: pointer; position: relative; }
.main-tab:hover { background: #ffffff; }
.main-tab.active { background: #84d669; border-color: #84d669;}
.main-tab.active .main-tab-content { color: white; }
.main-tab-content { display: flex; align-items: center; gap: 4px; font-family: 'Fira Code', monospace; font-size: 14px; font-weight: 500; color: #3b3b3b; }
/* Workspace (Where windows float) */
.workspace { flex: 1; display: flex; flex-direction: column; overflow: hidden; background: white; 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: white; border: 1px solid #f0f1f2; 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: #84d669; }
.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: #3b3b3b; margin-top: auto; }
/* The background abstract pattern from BUILD V3 */
.bg-grid { position: absolute; inset: 0; background-image: linear-gradient(to right, #f0fdf4 1px, transparent 1px), linear-gradient(to bottom, #f0fdf4 1px, transparent 1px); background-size: 40px 40px; z-index: 0; pointer-events: none; }
.bg-svg { position: absolute; top: -10%; left: -10%; width: 120%; height: 120%; z-index: 0; opacity: 0.6; pointer-events: none; display: block !important;}
.bg-svg path { fill: none; stroke: #e6f2eb; stroke-width: 45; stroke-linecap: round; stroke-linejoin: round; }
.bg-svg path.inner { stroke: #f7faf9; stroke-width: 41; }
/* Bottom Taskbar */
.toolbar { height: 50px; background: white; border-top: 1px solid #f0f1f2; 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: #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: #f8f8f8; }
.taskbar-item.active { border-bottom-color: #84d669; background: linear-gradient(to bottom, rgba(132,214,105,0) 50%, 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="#3b3b3b" 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="#3b3b3b" 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="bg-grid"></div>
<svg class="bg-svg" preserveAspectRatio="xMidYMid slice" viewBox="0 0 1000 600">
<path d="M-50,200 Q200,100 400,250 T800,50 T1100,250" />
<path d="M100,-50 Q250,200 150,450 T400,650" />
<path d="M500,-50 Q450,250 800,350 T750,700" />
<path class="inner" d="M-50,200 Q200,100 400,250 T800,50 T1100,250" />
<path class="inner" d="M100,-50 Q250,200 150,450 T400,650" />
<path class="inner" d="M500,-50 Q450,250 800,350 T750,700" />
</svg>
<div class="panel-section">
<div class="panel-grid">
<!-- HTMX Enabled Desktop Icons that WindowManager catches -->
<div class="desktop-icon" data-app-id="vibe" data-app-title="Vibe" 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="#84d669" stroke-width="2"/>
<path d="M14 21h14M21 14v14" stroke="#84d669" stroke-width="2"/>
</svg>
<div class="panel-card-label">Mantis</div>
</div>
<div class="desktop-icon" data-app-id="tasks" data-app-title="Tasks" hx-get="/suite/partials/tasks.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="#3b3b3b" stroke-width="2"/>
<line x1="12" y1="12" x2="30" y2="12" stroke="#3b3b3b" stroke-width="2"/>
<line x1="12" y1="21" x2="30" y2="21" stroke="#3b3b3b" stroke-width="2"/>
<line x1="12" y1="30" x2="24" y2="30" stroke="#3b3b3b" 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="#3b3b3b" stroke-width="2"/>
<line x1="10" y1="14" x2="32" y2="14" stroke="#3b3b3b" stroke-width="2"/>
<line x1="10" y1="22" x2="28" y2="22" stroke="#3b3b3b" stroke-width="2"/>
<line x1="10" y1="30" x2="24" y2="30" stroke="#3b3b3b" 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="#3b3b3b" stroke-width="2"/>
<line x1="10" y1="10" x2="32" y2="10" stroke="#3b3b3b" stroke-width="2"/>
<line x1="10" y1="18" x2="28" y2="18" stroke="#3b3b3b" stroke-width="2"/>
<line x1="10" y1="26" x2="32" y2="26" stroke="#3b3b3b" stroke-width="2"/>
<line x1="10" y1="34" x2="24" y2="34" stroke="#3b3b3b" 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="#3b3b3b" stroke-width="2"/>
<line x1="12" y1="2" x2="12" y2="24" stroke="#3b3b3b" stroke-width="2"/>
<polyline points="22 16 30 10 38 16" stroke="#3b3b3b" stroke-width="2"/>
<line x1="30" y1="10" x2="30" y2="32" stroke="#3b3b3b" 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="#3b3b3b" stroke-width="2"/>
<circle cx="14" cy="16" r="4" stroke="#3b3b3b" stroke-width="2"/>
<path d="M6 34a6 6 0 0 1 6-6h10a6 6 0 0 1 6 6v2H6v-2z" stroke="#3b3b3b" 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">
<div id="clock-time">00:00</div>
<div id="clock-date">01/01/2026</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

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>

1180
ui/suite/partials/vibe.html Normal file

File diff suppressed because it is too large Load diff

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

@ -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;

File diff suppressed because it is too large Load diff

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

Binary file not shown.

Binary file not shown.

Binary file not shown.