style: Format vibe.html for better readability
All checks were successful
BotUI CI / build (push) Successful in 2m4s

- Improve indentation and line breaks in vibe.html
- No functional changes, only code formatting
This commit is contained in:
Rodrigo Rodriguez (Pragmatismo) 2026-03-01 22:36:15 -03:00
parent 0c2dd80f30
commit dd6e1aa2bc
12 changed files with 1326 additions and 518 deletions

View file

@ -134,28 +134,28 @@ const ROOT_FILES: &[&str] = &[
pub async fn index(OriginalUri(uri): OriginalUri) -> Response { pub async fn index(OriginalUri(uri): OriginalUri) -> Response {
let path = uri.path(); let path = uri.path();
// Check if path contains static asset directories - serve them directly // Check if path contains static asset directories - serve them directly
let path_lower = path.to_lowercase(); let path_lower = path.to_lowercase();
if path_lower.contains("/js/") if path_lower.contains("/js/")
|| path_lower.contains("/css/") || path_lower.contains("/css/")
|| path_lower.contains("/vendor/") || path_lower.contains("/vendor/")
|| path_lower.contains("/assets/") || path_lower.contains("/assets/")
|| path_lower.contains("/public/") || path_lower.contains("/public/")
|| path_lower.contains("/partials/") || path_lower.contains("/partials/")
|| path_lower.ends_with(".js") || path_lower.ends_with(".js")
|| path_lower.ends_with(".css") || path_lower.ends_with(".css")
|| path_lower.ends_with(".png") || path_lower.ends_with(".png")
|| path_lower.ends_with(".jpg") || path_lower.ends_with(".jpg")
|| path_lower.ends_with(".jpeg") || path_lower.ends_with(".jpeg")
|| path_lower.ends_with(".gif") || path_lower.ends_with(".gif")
|| path_lower.ends_with(".svg") || path_lower.ends_with(".svg")
|| path_lower.ends_with(".ico") || path_lower.ends_with(".ico")
|| path_lower.ends_with(".woff") || path_lower.ends_with(".woff")
|| path_lower.ends_with(".woff2") || path_lower.ends_with(".woff2")
|| path_lower.ends_with(".ttf") || path_lower.ends_with(".ttf")
|| path_lower.ends_with(".eot") || path_lower.ends_with(".eot")
|| path_lower.ends_with(".mp4") || path_lower.ends_with(".mp4")
|| path_lower.ends_with(".webm") || path_lower.ends_with(".webm")
|| path_lower.ends_with(".mp3") || path_lower.ends_with(".mp3")
|| path_lower.ends_with(".wav") || path_lower.ends_with(".wav")
@ -182,11 +182,11 @@ pub async fn index(OriginalUri(uri): OriginalUri) -> Response {
} else { } else {
path.to_string() path.to_string()
}; };
let full_path = get_ui_root().join(&fs_path); let full_path = get_ui_root().join(&fs_path);
info!("index: Serving static file: {} -> {:?} (fs_path: {})", path, full_path, fs_path); info!("index: Serving static file: {} -> {:?} (fs_path: {})", path, full_path, fs_path);
#[cfg(feature = "embed-ui")] #[cfg(feature = "embed-ui")]
{ {
let asset_path = fs_path.trim_start_matches('/'); let asset_path = fs_path.trim_start_matches('/');
@ -195,7 +195,7 @@ pub async fn index(OriginalUri(uri): OriginalUri) -> Response {
return ([(axum::http::header::CONTENT_TYPE, mime.as_ref())], content.data).into_response(); return ([(axum::http::header::CONTENT_TYPE, mime.as_ref())], content.data).into_response();
} }
} }
#[cfg(not(feature = "embed-ui"))] #[cfg(not(feature = "embed-ui"))]
{ {
if let Ok(bytes) = tokio::fs::read(&full_path).await { if let Ok(bytes) = tokio::fs::read(&full_path).await {
@ -203,11 +203,11 @@ pub async fn index(OriginalUri(uri): OriginalUri) -> Response {
return (StatusCode::OK, [("content-type", mime.as_ref())], bytes).into_response(); return (StatusCode::OK, [("content-type", mime.as_ref())], bytes).into_response();
} }
} }
warn!("index: Static file not found: {} -> {:?}", path, full_path); warn!("index: Static file not found: {} -> {:?}", path, full_path);
return StatusCode::NOT_FOUND.into_response(); return StatusCode::NOT_FOUND.into_response();
} }
let path_parts: Vec<&str> = path.split('/').collect(); let path_parts: Vec<&str> = path.split('/').collect();
let bot_name = path_parts let bot_name = path_parts
.iter() .iter()
@ -770,20 +770,20 @@ struct ClientError {
async fn handle_client_error(Json(error): Json<ClientError>) -> impl IntoResponse { async fn handle_client_error(Json(error): Json<ClientError>) -> impl IntoResponse {
warn!( warn!(
"CLIENT:{}: {} at {} ({}) - {}", "CLIENT:{}: {} at {} ({}) - {}",
error.source.to_uppercase(), error.source.to_uppercase(),
error.message, error.message,
error.url, error.url,
error.timestamp, error.timestamp,
error.user_agent error.user_agent
); );
if let Some(stack) = &error.stack { if let Some(stack) = &error.stack {
if !stack.is_empty() { if !stack.is_empty() {
warn!("CLIENT:STACK: {}", stack); warn!("CLIENT:STACK: {}", stack);
} }
} }
StatusCode::OK StatusCode::OK
} }
@ -1182,6 +1182,76 @@ async fn handle_auth_asset(axum::extract::Path(path): axum::extract::Path<String
} }
} }
/// Serve login page at clean /login route (hides physical path /suite/auth/login.html)
async fn serve_login() -> impl IntoResponse {
#[cfg(feature = "embed-ui")]
{
let asset_path = "suite/auth/login.html";
match Assets::get(asset_path) {
Some(content) => {
let mime = mime_guess::from_path(asset_path).first_or_octet_stream();
(
[(axum::http::header::CONTENT_TYPE, mime.as_ref())],
content.data,
)
.into_response()
}
None => StatusCode::NOT_FOUND.into_response(),
}
}
#[cfg(not(feature = "embed-ui"))]
{
let login_path = get_ui_root().join("suite/auth/login.html");
match tokio::fs::read(&login_path).await {
Ok(content) => {
let mime = mime_guess::from_path(&login_path).first_or_octet_stream();
(
[(axum::http::header::CONTENT_TYPE, mime.as_ref())],
content,
)
.into_response()
}
Err(_) => StatusCode::NOT_FOUND.into_response(),
}
}
}
/// Serve logout page at clean /logout route (hides physical path /suite/auth/logout.html)
async fn serve_logout() -> impl IntoResponse {
#[cfg(feature = "embed-ui")]
{
let asset_path = "suite/auth/logout.html";
match Assets::get(asset_path) {
Some(content) => {
let mime = mime_guess::from_path(asset_path).first_or_octet_stream();
(
[(axum::http::header::CONTENT_TYPE, mime.as_ref())],
content.data,
)
.into_response()
}
None => StatusCode::NOT_FOUND.into_response(),
}
}
#[cfg(not(feature = "embed-ui"))]
{
let logout_path = get_ui_root().join("suite/auth/logout.html");
match tokio::fs::read(&logout_path).await {
Ok(content) => {
let mime = mime_guess::from_path(&logout_path).first_or_octet_stream();
(
[(axum::http::header::CONTENT_TYPE, mime.as_ref())],
content,
)
.into_response()
}
Err(_) => StatusCode::NOT_FOUND.into_response(),
}
}
}
fn add_static_routes(router: Router<AppState>, _suite_path: &Path) -> Router<AppState> { fn add_static_routes(router: Router<AppState>, _suite_path: &Path) -> Router<AppState> {
#[cfg(feature = "embed-ui")] #[cfg(feature = "embed-ui")]
{ {
@ -1219,6 +1289,8 @@ pub fn configure_router() -> Router {
let mut router = Router::new() let mut router = Router::new()
.route("/health", get(health)) .route("/health", get(health))
.route("/favicon.ico", get(serve_favicon)) .route("/favicon.ico", get(serve_favicon))
.route("/login", get(serve_login))
.route("/logout", get(serve_logout))
.nest("/api", create_api_router()) .nest("/api", create_api_router())
.nest("/ui", create_ui_router()) .nest("/ui", create_ui_router())
.nest("/ws", create_ws_router()) .nest("/ws", create_ws_router())

View file

@ -153,7 +153,7 @@
<script> <script>
// Configuration // Configuration
const CONFIG = { const CONFIG = {
serverUrl: window.BOTSERVER_URL || 'http://localhost:9000', serverUrl: window.BOTSERVER_URL || 'http://localhost:8080',
maxMessages: 10, // Keep memory low maxMessages: 10, // Keep memory low
maxMsgLen: 100, // Truncate long messages maxMsgLen: 100, // Truncate long messages
}; };

View file

@ -172,7 +172,7 @@
<div class="activity-list" hx-get="/api/admin/dashboard/activity" hx-trigger="load" hx-swap="innerHTML"> <div class="activity-list" hx-get="/api/admin/dashboard/activity" hx-trigger="load" hx-swap="innerHTML">
<div class="activity-item"> <div class="activity-item">
<div class="activity-avatar"> <div class="activity-avatar">
<img src="/assets/avatars/default.svg" alt="User avatar"> <img src="/suite/assets/avatars/default.svg" alt="User avatar">
</div> </div>
<div class="activity-content"> <div class="activity-content">
<div class="activity-text"> <div class="activity-text">
@ -198,7 +198,7 @@
</div> </div>
<div class="activity-item"> <div class="activity-item">
<div class="activity-avatar"> <div class="activity-avatar">
<img src="/assets/avatars/default.svg" alt="User avatar"> <img src="/suite/assets/avatars/default.svg" alt="User avatar">
</div> </div>
<div class="activity-content"> <div class="activity-content">
<div class="activity-text"> <div class="activity-text">
@ -222,7 +222,7 @@
</div> </div>
<div class="activity-item"> <div class="activity-item">
<div class="activity-avatar"> <div class="activity-avatar">
<img src="/assets/avatars/default.svg" alt="User avatar"> <img src="/suite/assets/avatars/default.svg" alt="User avatar">
</div> </div>
<div class="activity-content"> <div class="activity-content">
<div class="activity-text"> <div class="activity-text">
@ -274,7 +274,7 @@
<div class="member-list" hx-get="/api/admin/dashboard/members" hx-trigger="load" hx-swap="innerHTML"> <div class="member-list" hx-get="/api/admin/dashboard/members" hx-trigger="load" hx-swap="innerHTML">
<div class="member-item"> <div class="member-item">
<div class="member-avatar"> <div class="member-avatar">
<img src="/assets/avatars/default.svg" alt="User avatar"> <img src="/suite/assets/avatars/default.svg" alt="User avatar">
<span class="status-indicator online"></span> <span class="status-indicator online"></span>
</div> </div>
<div class="member-info"> <div class="member-info">
@ -285,7 +285,7 @@
</div> </div>
<div class="member-item"> <div class="member-item">
<div class="member-avatar"> <div class="member-avatar">
<img src="/assets/avatars/default.svg" alt="User avatar"> <img src="/suite/assets/avatars/default.svg" alt="User avatar">
<span class="status-indicator online"></span> <span class="status-indicator online"></span>
</div> </div>
<div class="member-info"> <div class="member-info">
@ -296,7 +296,7 @@
</div> </div>
<div class="member-item"> <div class="member-item">
<div class="member-avatar"> <div class="member-avatar">
<img src="/assets/avatars/default.svg" alt="User avatar"> <img src="/suite/assets/avatars/default.svg" alt="User avatar">
<span class="status-indicator away"></span> <span class="status-indicator away"></span>
</div> </div>
<div class="member-info"> <div class="member-info">
@ -307,7 +307,7 @@
</div> </div>
<div class="member-item"> <div class="member-item">
<div class="member-avatar"> <div class="member-avatar">
<img src="/assets/avatars/default.svg" alt="User avatar"> <img src="/suite/assets/avatars/default.svg" alt="User avatar">
<span class="status-indicator offline"></span> <span class="status-indicator offline"></span>
</div> </div>
<div class="member-info"> <div class="member-info">

View file

@ -2,7 +2,7 @@
<div class="wizard-container"> <div class="wizard-container">
<div class="wizard-sidebar"> <div class="wizard-sidebar">
<div class="wizard-logo"> <div class="wizard-logo">
<img src="/assets/icons/gb-logo.svg" alt="General Bots" width="48" height="48"> <img src="/suite/assets/icons/gb-logo.svg" alt="General Bots" width="48" height="48">
</div> </div>
<div class="wizard-steps"> <div class="wizard-steps">
<div class="wizard-step active" data-step="1"> <div class="wizard-step active" data-step="1">

View file

@ -283,7 +283,7 @@
<body> <body>
<div class="setup-container"> <div class="setup-container">
<div class="setup-header"> <div class="setup-header">
<div class="setup-logo">🤖</div> <div class="setup-logo"><img src="/suite/assets/icons/gb-logo.svg" alt="General Bots" width="48" height="48"></div>
<h1 class="setup-title">Initial Setup</h1> <h1 class="setup-title">Initial Setup</h1>
<p class="setup-subtitle"> <p class="setup-subtitle">
Create the first administrator account for your General Bots installation Create the first administrator account for your General Bots installation

View file

@ -599,7 +599,7 @@
<div class="login-header"> <div class="login-header">
<div class="login-logo" id="login-logo"> <div class="login-logo" id="login-logo">
<img id="login-logo-img" src="" alt="Logo" style="display:none; width:100%; height:100%; object-fit:contain;"> <img id="login-logo-img" src="" alt="Logo" style="display:none; width:100%; height:100%; object-fit:contain;">
<span id="login-logo-default">🤖</span> <span id="login-logo-default"><img src="/suite/assets/icons/gb-logo.svg" alt="General Bots" width="48" height="48"></span>
</div> </div>
<h1 class="login-title" id="login-title">Welcome Back</h1> <h1 class="login-title" id="login-title">Welcome Back</h1>
<p class="login-subtitle" id="login-subtitle"> <p class="login-subtitle" id="login-subtitle">

View file

@ -557,7 +557,7 @@
<body> <body>
<div class="register-container"> <div class="register-container">
<div class="register-header"> <div class="register-header">
<div class="register-logo">🤖</div> <div class="register-logo"><img src="/suite/assets/icons/gb-logo.svg" alt="General Bots" width="48" height="48"></div>
<h1 class="register-title">Create Account</h1> <h1 class="register-title">Create Account</h1>
<p class="register-subtitle"> <p class="register-subtitle">
Join General Bots and start building Join General Bots and start building

View file

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

View file

@ -6,7 +6,7 @@
<header class="meet-header"> <header class="meet-header">
<div class="meet-brand"> <div class="meet-brand">
<img <img
src="/assets/icons/gb-meet.svg" src="/suite/assets/icons/gb-meet.svg"
alt="Meet" alt="Meet"
class="brand-icon" class="brand-icon"
/> />

View file

@ -1075,9 +1075,9 @@
var temp = document.createElement("div"); var temp = document.createElement("div");
temp.style.color = hexcolor; temp.style.color = hexcolor;
temp.style.display = "none"; temp.style.display = "none";
document.div.appendChild(temp); document.body.appendChild(temp);
var style = window.getComputedStyle(temp).color; var style = window.getComputedStyle(temp).color;
document.div.removeChild(temp); document.body.removeChild(temp);
var rgb = style.match(/\d+/g); var rgb = style.match(/\d+/g);
if (!rgb || rgb.length < 3) return '#ffffff'; if (!rgb || rgb.length < 3) return '#ffffff';

View file

@ -66,8 +66,79 @@
</svg> </svg>
<span>Agent Farm</span> <span>Agent Farm</span>
</div> </div>
<div class="minibar-actions"> <div class="minibar-actions" style="position: relative;">
<button class="minibar-btn" title="Settings" type="button"></button> <button class="minibar-btn" title="Settings" type="button"></button>
<button class="minibar-btn" title="Account" type="button">👤</button> <div class="user-menu-container" style="position: relative;">
<button class="minibar-btn" id="userAvatar" title="Account" type="button"
style="border-radius: 50%; width: 22px; height: 22px; background: #ddd; color: #555; font-size: 11px; font-weight: bold; overflow: hidden; display: flex; align-items: center; justify-content: center; margin-left: 4px;"><span>U</span></button>
<div id="userDropdown"
style="display: none; position: absolute; right: 0; top: 100%; margin-top: 4px; background: white; border: 1px solid #f0f1f2; border-radius: 6px; box-shadow: 0 4px 12px rgba(0,0,0,0.1); width: 220px; padding: 12px; z-index: 10000; flex-direction: column; gap: 8px;">
<div
style="display: flex; flex-direction: column; border-bottom: 1px solid #f0f1f2; padding-bottom: 8px;">
<span id="userName" style="font-weight: 600; color: #3b3b3b; font-size: 13px;">User</span>
<span id="userEmail" style="color: #888; font-size: 11px;">user@example.com</span>
</div>
<a id="authAction" href="/auth/login.html"
style="display: flex; align-items: center; gap: 6px; text-decoration: none; padding: 6px; border-radius: 4px; font-size: 12px; transition: background 0.15s; cursor: pointer; color: var(--primary, #84d669);">
<svg id="authIcon" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor"
stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M15 3h4a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2h-4"></path>
<polyline points="10 17 15 12 21 12"></polyline>
<line x1="15" y1="12" x2="3" y2="12"></line>
</svg>
<span id="authText">Sign in</span>
</a>
</div>
</div>
</div> </div>
</div> </div>
<script>
(function () {
var avatar = document.getElementById('userAvatar');
var dropdown = document.getElementById('userDropdown');
avatar.addEventListener('click', function (e) {
e.stopPropagation();
dropdown.style.display = dropdown.style.display === 'none' ? 'flex' : 'none';
});
document.addEventListener('click', function (e) {
if (!e.target.closest('.user-menu-container')) {
dropdown.style.display = 'none';
}
});
// Trigger manual load or UI sync since minibar loads asynchronously via HTMX
setTimeout(function () {
var token = localStorage.getItem("gb-access-token") || sessionStorage.getItem("gb-access-token");
if (token) {
fetch("/api/auth/me", { headers: { Authorization: "Bearer " + token } })
.then(res => res.json())
.then(user => {
document.getElementById('userName').textContent = user.display_name || user.first_name || user.username || "User";
document.getElementById('userEmail').textContent = user.email || "";
var btn = document.getElementById('authAction');
btn.href = "#";
btn.style.color = "var(--error, #ef4444)";
document.getElementById('authText').textContent = "Sign out";
document.getElementById('authIcon').innerHTML = '<path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"></path><polyline points="16 17 21 12 16 7"></polyline><line x1="21" y1="12" x2="9" y2="12"></line>';
btn.onclick = function (e) {
e.preventDefault();
fetch("/api/auth/logout", { method: "POST" }).finally(function () {
localStorage.removeItem("gb-access-token");
sessionStorage.removeItem("gb-access-token");
window.location.href = "/auth/login.html";
});
};
var initial = (user.display_name || user.username || "U").charAt(0).toUpperCase();
document.getElementById('userAvatar').innerHTML = '<span>' + initial + '</span>';
})
.catch(console.error);
}
}, 500);
})();
</script>

File diff suppressed because it is too large Load diff