Update UI components, styling, and add theme-sentient and intents

This commit is contained in:
Rodrigo Rodriguez (Pragmatismo) 2025-12-15 23:16:09 -03:00
parent db06e42289
commit 9fe234aa3c
28 changed files with 5140 additions and 1791 deletions

View file

@ -18,8 +18,7 @@ use log::{debug, error, info};
use serde::Deserialize; use serde::Deserialize;
use std::{fs, path::PathBuf}; use std::{fs, path::PathBuf};
use tokio_tungstenite::{ use tokio_tungstenite::{
connect_async_tls_with_config, connect_async_tls_with_config, tungstenite::protocol::Message as TungsteniteMessage,
tungstenite::protocol::Message as TungsteniteMessage,
}; };
use crate::shared::AppState; use crate::shared::AppState;
@ -99,7 +98,10 @@ async fn proxy_api(
req: Request<Body>, req: Request<Body>,
) -> Response<Body> { ) -> Response<Body> {
let path = original_uri.path(); let path = original_uri.path();
let query = original_uri.query().map(|q| format!("?{}", q)).unwrap_or_default(); let query = original_uri
.query()
.map(|q| format!("?{}", q))
.unwrap_or_default();
let method = req.method().clone(); let method = req.method().clone();
let headers = req.headers().clone(); let headers = req.headers().clone();
@ -217,7 +219,11 @@ async fn ws_proxy(
async fn handle_ws_proxy(client_socket: WebSocket, state: AppState, params: WsQuery) { async fn handle_ws_proxy(client_socket: WebSocket, state: AppState, params: WsQuery) {
let backend_url = format!( let backend_url = format!(
"{}/ws?session_id={}&user_id={}", "{}/ws?session_id={}&user_id={}",
state.client.base_url().replace("https://", "wss://").replace("http://", "ws://"), state
.client
.base_url()
.replace("https://", "wss://")
.replace("http://", "ws://"),
params.session_id, params.session_id,
params.user_id params.user_id
); );
@ -234,12 +240,8 @@ async fn handle_ws_proxy(client_socket: WebSocket, state: AppState, params: WsQu
let connector = tokio_tungstenite::Connector::NativeTls(tls_connector); let connector = tokio_tungstenite::Connector::NativeTls(tls_connector);
// Connect to backend WebSocket // Connect to backend WebSocket
let backend_result = connect_async_tls_with_config( let backend_result =
&backend_url, connect_async_tls_with_config(&backend_url, None, false, Some(connector)).await;
None,
false,
Some(connector),
).await;
let backend_socket = match backend_result { let backend_socket = match backend_result {
Ok((socket, _)) => socket, Ok((socket, _)) => socket,
@ -260,22 +262,38 @@ async fn handle_ws_proxy(client_socket: WebSocket, state: AppState, params: WsQu
while let Some(msg) = client_rx.next().await { while let Some(msg) = client_rx.next().await {
match msg { match msg {
Ok(AxumMessage::Text(text)) => { Ok(AxumMessage::Text(text)) => {
if backend_tx.send(TungsteniteMessage::Text(text)).await.is_err() { if backend_tx
.send(TungsteniteMessage::Text(text))
.await
.is_err()
{
break; break;
} }
} }
Ok(AxumMessage::Binary(data)) => { Ok(AxumMessage::Binary(data)) => {
if backend_tx.send(TungsteniteMessage::Binary(data)).await.is_err() { if backend_tx
.send(TungsteniteMessage::Binary(data))
.await
.is_err()
{
break; break;
} }
} }
Ok(AxumMessage::Ping(data)) => { Ok(AxumMessage::Ping(data)) => {
if backend_tx.send(TungsteniteMessage::Ping(data)).await.is_err() { if backend_tx
.send(TungsteniteMessage::Ping(data))
.await
.is_err()
{
break; break;
} }
} }
Ok(AxumMessage::Pong(data)) => { Ok(AxumMessage::Pong(data)) => {
if backend_tx.send(TungsteniteMessage::Pong(data)).await.is_err() { if backend_tx
.send(TungsteniteMessage::Pong(data))
.await
.is_err()
{
break; break;
} }
} }
@ -323,8 +341,32 @@ async fn handle_ws_proxy(client_socket: WebSocket, state: AppState, params: WsQu
/// Create WebSocket proxy router /// Create WebSocket proxy router
fn create_ws_router() -> Router<AppState> { fn create_ws_router() -> Router<AppState> {
Router::new().fallback(any(ws_proxy))
}
/// Create UI HTMX proxy router (for HTML fragment endpoints)
fn create_ui_router() -> Router<AppState> {
Router::new() Router::new()
.fallback(get(ws_proxy)) // Email UI endpoints
.route("/email/accounts", any(proxy_api))
.route("/email/list", any(proxy_api))
.route("/email/folders", any(proxy_api))
.route("/email/compose", any(proxy_api))
.route("/email/labels", any(proxy_api))
.route("/email/templates", any(proxy_api))
.route("/email/signatures", any(proxy_api))
.route("/email/rules", any(proxy_api))
.route("/email/search", any(proxy_api))
.route("/email/auto-responder", any(proxy_api))
.route("/email/{id}", any(proxy_api))
.route("/email/{id}/delete", any(proxy_api))
// Calendar UI endpoints
.route("/calendar/list", any(proxy_api))
.route("/calendar/upcoming", any(proxy_api))
.route("/calendar/event/new", any(proxy_api))
.route("/calendar/new", any(proxy_api))
// Fallback for any other /ui/* routes
.fallback(any(proxy_api))
} }
/// Configure and return the main router /// Configure and return the main router
@ -338,6 +380,8 @@ pub fn configure_router() -> Router {
.route("/health", get(health)) .route("/health", get(health))
// API proxy routes // API proxy routes
.nest("/api", create_api_router()) .nest("/api", create_api_router())
// UI HTMX proxy routes (for /ui/* endpoints that return HTML fragments)
.nest("/ui", create_ui_router())
// WebSocket proxy routes // WebSocket proxy routes
.nest("/ws", create_ws_router()) .nest("/ws", create_ws_router())
// UI routes // UI routes
@ -373,6 +417,62 @@ pub fn configure_router() -> Router {
"/suite/tasks", "/suite/tasks",
tower_http::services::ServeDir::new(suite_path.join("tasks")), tower_http::services::ServeDir::new(suite_path.join("tasks")),
) )
.nest_service(
"/suite/calendar",
tower_http::services::ServeDir::new(suite_path.join("calendar")),
)
.nest_service(
"/suite/meet",
tower_http::services::ServeDir::new(suite_path.join("meet")),
)
.nest_service(
"/suite/paper",
tower_http::services::ServeDir::new(suite_path.join("paper")),
)
.nest_service(
"/suite/research",
tower_http::services::ServeDir::new(suite_path.join("research")),
)
.nest_service(
"/suite/analytics",
tower_http::services::ServeDir::new(suite_path.join("analytics")),
)
.nest_service(
"/suite/monitoring",
tower_http::services::ServeDir::new(suite_path.join("monitoring")),
)
.nest_service(
"/suite/admin",
tower_http::services::ServeDir::new(suite_path.join("admin")),
)
.nest_service(
"/suite/auth",
tower_http::services::ServeDir::new(suite_path.join("auth")),
)
.nest_service(
"/suite/settings",
tower_http::services::ServeDir::new(suite_path.join("settings")),
)
.nest_service(
"/suite/sources",
tower_http::services::ServeDir::new(suite_path.join("sources")),
)
.nest_service(
"/suite/attendant",
tower_http::services::ServeDir::new(suite_path.join("attendant")),
)
.nest_service(
"/suite/tools",
tower_http::services::ServeDir::new(suite_path.join("tools")),
)
.nest_service(
"/suite/assets",
tower_http::services::ServeDir::new(suite_path.join("assets")),
)
.nest_service(
"/suite/partials",
tower_http::services::ServeDir::new(suite_path.join("partials")),
)
// Legacy paths for backward compatibility (serve suite assets) // Legacy paths for backward compatibility (serve suite assets)
.nest_service( .nest_service(
"/js", "/js",

View file

@ -4,19 +4,20 @@
.admin-layout { .admin-layout {
display: grid; display: grid;
grid-template-columns: 260px 1fr; grid-template-columns: 260px 1fr;
min-height: calc(100vh - 56px); height: 100%;
background: var(--bg); min-height: 0;
background: var(--bg, var(--bg-primary, #0a0a0f));
color: var(--text, var(--text-primary, #ffffff));
} }
/* Sidebar */ /* Sidebar */
.admin-sidebar { .admin-sidebar {
background: var(--surface); background: var(--surface, var(--bg-secondary, #12121a));
border-right: 1px solid var(--border); border-right: 1px solid var(--border, #2a2a3a);
display: flex; display: flex;
flex-direction: column; flex-direction: column;
position: sticky; overflow-y: auto;
top: 56px; height: 100%;
height: calc(100vh - 56px);
} }
.admin-header { .admin-header {
@ -138,10 +139,22 @@
justify-content: center; justify-content: center;
} }
.stat-icon.users { background: rgba(59, 130, 246, 0.1); color: #3b82f6; } .stat-icon.users {
.stat-icon.groups { background: rgba(16, 185, 129, 0.1); color: #10b981; } background: rgba(59, 130, 246, 0.1);
.stat-icon.bots { background: rgba(168, 85, 247, 0.1); color: #a855f7; } color: #3b82f6;
.stat-icon.storage { background: rgba(249, 115, 22, 0.1); color: #f97316; } }
.stat-icon.groups {
background: rgba(16, 185, 129, 0.1);
color: #10b981;
}
.stat-icon.bots {
background: rgba(168, 85, 247, 0.1);
color: #a855f7;
}
.stat-icon.storage {
background: rgba(249, 115, 22, 0.1);
color: #f97316;
}
.stat-content { .stat-content {
display: flex; display: flex;
@ -287,9 +300,15 @@
border-radius: 50%; border-radius: 50%;
} }
.health-status.healthy { background: #10b981; } .health-status.healthy {
.health-status.warning { background: #f59e0b; } background: #10b981;
.health-status.error { background: #ef4444; } }
.health-status.warning {
background: #f59e0b;
}
.health-status.error {
background: #ef4444;
}
.health-value { .health-value {
font-size: 24px; font-size: 24px;
@ -458,7 +477,9 @@
} }
@keyframes spin { @keyframes spin {
to { transform: rotate(360deg); } to {
transform: rotate(360deg);
}
} }
/* Responsive */ /* Responsive */

View file

@ -2,8 +2,10 @@
.attendant-container { .attendant-container {
display: flex; display: flex;
height: calc(100vh - 60px); height: 100%;
background: var(--bg); min-height: 0;
background: var(--bg, var(--bg-primary, #0a0a0f));
color: var(--text, var(--text-primary, #ffffff));
} }
/* Queue Panel */ /* Queue Panel */
@ -300,10 +302,18 @@
margin-right: 6px; margin-right: 6px;
} }
.status-dot.online { background: #22c55e; } .status-dot.online {
.status-dot.away { background: #f59e0b; } background: #22c55e;
.status-dot.busy { background: #ef4444; } }
.status-dot.offline { background: #6b7280; } .status-dot.away {
background: #f59e0b;
}
.status-dot.busy {
background: #ef4444;
}
.status-dot.offline {
background: #6b7280;
}
/* Responsive */ /* Responsive */
@media (max-width: 1024px) { @media (max-width: 1024px) {

View file

@ -362,182 +362,64 @@
<!-- Theme Selection --> <!-- Theme Selection -->
<div class="settings-section"> <div class="settings-section">
<div class="settings-section-title">Theme</div> <div class="settings-section-title">Theme</div>
<div class="theme-grid"> <select id="themeSelector" class="theme-dropdown">
<div <optgroup label="Core Themes">
class="theme-option theme-dark" <option value="sentient">
data-theme="dark" 🤖 Sentient
title="Dark" </option>
> <option value="dark">🌑 Dark</option>
<div class="theme-option-inner"> <option value="light">☀️ Light</option>
<div class="theme-option-header"> <option value="blue">🌊 Ocean</option>
<div class="theme-option-dot"></div> <option value="purple">💜 Violet</option>
<div class="theme-option-dot"></div> <option value="green">🌲 Forest</option>
<div class="theme-option-dot"></div> <option value="orange">🌅 Sunset</option>
</div> </optgroup>
<div class="theme-option-body"> <optgroup label="Retro Themes">
<div <option value="cyberpunk">
class="theme-option-line" 🌃 Cyberpunk
style="width: 80%" </option>
></div> <option value="retrowave">
<div 🌴 Retrowave
class="theme-option-line" </option>
style="width: 60%" <option value="vapordream">
></div> 💭 Vapor Dream
<div </option>
class="theme-option-line" <option value="y2kglow">✨ Y2K</option>
style="width: 70%" <option value="arcadeflash">
></div> 🕹️ Arcade
</div> </option>
</div> <option value="discofever">🪩 Disco</option>
<span class="theme-option-name">Dark</span> <option value="grungeera">🎸 Grunge</option>
</div> </optgroup>
<div <optgroup label="Classic Themes">
class="theme-option theme-light" <option value="jazzage">🎺 Jazz Age</option>
data-theme="light" <option value="mellowgold">
title="Light" 🌻 Mellow Gold
> </option>
<div class="theme-option-inner"> <option value="midcenturymod">
<div class="theme-option-header"> 🏠 Mid Century
<div class="theme-option-dot"></div> </option>
<div class="theme-option-dot"></div> <option value="polaroidmemories">
<div class="theme-option-dot"></div> 📷 Polaroid
</div> </option>
<div class="theme-option-body"> <option value="saturdaycartoons">
<div 📺 Cartoons
class="theme-option-line" </option>
style="width: 80%" <option value="seasidepostcard">
></div> 🏖️ Seaside
<div </option>
class="theme-option-line" <option value="typewriter">
style="width: 60%" ⌨️ Typewriter
></div> </option>
<div </optgroup>
class="theme-option-line" <optgroup label="Tech Themes">
style="width: 70%" <option value="3dbevel">🔲 3D Bevel</option>
></div> <option value="xeroxui">📠 Xerox UI</option>
</div> <option value="xtreegold">
</div> 📁 XTree Gold
<span class="theme-option-name">Light</span> </option>
</div> </optgroup>
<div </select>
class="theme-option theme-blue"
data-theme="blue"
title="Ocean"
>
<div class="theme-option-inner">
<div class="theme-option-header">
<div class="theme-option-dot"></div>
<div class="theme-option-dot"></div>
<div class="theme-option-dot"></div>
</div>
<div class="theme-option-body">
<div
class="theme-option-line"
style="width: 80%"
></div>
<div
class="theme-option-line"
style="width: 60%"
></div>
<div
class="theme-option-line"
style="width: 70%"
></div>
</div>
</div>
<span class="theme-option-name">Ocean</span>
</div>
<div
class="theme-option theme-purple"
data-theme="purple"
title="Violet"
>
<div class="theme-option-inner">
<div class="theme-option-header">
<div class="theme-option-dot"></div>
<div class="theme-option-dot"></div>
<div class="theme-option-dot"></div>
</div>
<div class="theme-option-body">
<div
class="theme-option-line"
style="width: 80%"
></div>
<div
class="theme-option-line"
style="width: 60%"
></div>
<div
class="theme-option-line"
style="width: 70%"
></div>
</div>
</div>
<span class="theme-option-name"
>Violet</span
>
</div>
<div
class="theme-option theme-green"
data-theme="green"
title="Forest"
>
<div class="theme-option-inner">
<div class="theme-option-header">
<div class="theme-option-dot"></div>
<div class="theme-option-dot"></div>
<div class="theme-option-dot"></div>
</div>
<div class="theme-option-body">
<div
class="theme-option-line"
style="width: 80%"
></div>
<div
class="theme-option-line"
style="width: 60%"
></div>
<div
class="theme-option-line"
style="width: 70%"
></div>
</div>
</div>
<span class="theme-option-name"
>Forest</span
>
</div>
<div
class="theme-option theme-orange"
data-theme="orange"
title="Sunset"
>
<div class="theme-option-inner">
<div class="theme-option-header">
<div class="theme-option-dot"></div>
<div class="theme-option-dot"></div>
<div class="theme-option-dot"></div>
</div>
<div class="theme-option-body">
<div
class="theme-option-line"
style="width: 80%"
></div>
<div
class="theme-option-line"
style="width: 60%"
></div>
<div
class="theme-option-line"
style="width: 70%"
></div>
</div>
</div>
<span class="theme-option-name"
>Sunset</span
>
</div>
</div>
</div> </div>
<div class="settings-divider"></div> <div class="settings-divider"></div>
@ -741,12 +623,21 @@
<p class="ai-status">Ready to help</p> <p class="ai-status">Ready to help</p>
</div> </div>
</div> </div>
<button class="ai-panel-toggle" onclick="toggleAIPanel()" aria-label="Close AI Panel"></button> <button
class="ai-panel-toggle"
onclick="toggleAIPanel()"
aria-label="Close AI Panel"
>
</button>
</div> </div>
<div class="ai-panel-messages" id="ai-messages"> <div class="ai-panel-messages" id="ai-messages">
<div class="ai-message assistant"> <div class="ai-message assistant">
<div class="ai-message-bubble">Olá! Sou seu assistente AI. Posso ajudar com qualquer tarefa nesta tela.</div> <div class="ai-message-bubble">
Olá! Sou seu assistente AI. Posso ajudar com
qualquer tarefa nesta tela.
</div>
</div> </div>
</div> </div>
@ -758,14 +649,30 @@
</div> </div>
<div class="ai-panel-input"> <div class="ai-panel-input">
<input type="text" class="ai-input" placeholder="Como posso ajudar?" id="ai-input" <input
onkeypress="if(event.key==='Enter')sendAIMessage()"> type="text"
<button class="ai-send-btn" onclick="sendAIMessage()" aria-label="Send message"></button> class="ai-input"
placeholder="Como posso ajudar?"
id="ai-input"
onkeypress="if(event.key==='Enter')sendAIMessage()"
/>
<button
class="ai-send-btn"
onclick="sendAIMessage()"
aria-label="Send message"
>
</button>
</div> </div>
</aside> </aside>
<!-- AI Panel Toggle Button (when panel is collapsed) --> <!-- AI Panel Toggle Button (when panel is collapsed) -->
<button class="ai-panel-fab" id="ai-fab" onclick="toggleAIPanel()" aria-label="Open AI Assistant"> <button
class="ai-panel-fab"
id="ai-fab"
onclick="toggleAIPanel()"
aria-label="Open AI Assistant"
>
🤖 🤖
</button> </button>
</main> </main>
@ -868,23 +775,26 @@
} }
}); });
// Theme handling // Theme handling with dropdown
const themeOptions = document.querySelectorAll(".theme-option"); const themeSelector = document.getElementById("themeSelector");
const savedTheme = localStorage.getItem("gb-theme") || "dark"; const savedTheme = localStorage.getItem("gb-theme") || "sentient";
document.body.setAttribute("data-theme", savedTheme); document.body.setAttribute("data-theme", savedTheme);
document
.querySelector(`.theme-option[data-theme="${savedTheme}"]`)
?.classList.add("active");
themeOptions.forEach((option) => { // Set dropdown to saved value
option.addEventListener("click", () => { if (themeSelector) {
const theme = option.getAttribute("data-theme"); themeSelector.value = savedTheme;
themeSelector.addEventListener("change", (e) => {
const theme = e.target.value;
document.body.setAttribute("data-theme", theme); document.body.setAttribute("data-theme", theme);
localStorage.setItem("gb-theme", theme); localStorage.setItem("gb-theme", theme);
themeOptions.forEach((o) => o.classList.remove("active"));
option.classList.add("active"); // Also notify ThemeManager if it exists
}); if (window.ThemeManager && window.ThemeManager.loadTheme) {
window.ThemeManager.loadTheme(theme);
}
}); });
}
function toggleQuickSetting(el) { function toggleQuickSetting(el) {
el.classList.toggle("active"); el.classList.toggle("active");
@ -1187,142 +1097,145 @@
// Quick actions per app // Quick actions per app
const aiQuickActions = { const aiQuickActions = {
drive: [ drive: [
{ label: 'Upload file', action: 'upload_file' }, { label: "Upload file", action: "upload_file" },
{ label: 'Create folder', action: 'create_folder' }, { label: "Create folder", action: "create_folder" },
{ label: 'Search files', action: 'search_files' }, { label: "Search files", action: "search_files" },
{ label: 'Share', action: 'share_item' } { label: "Share", action: "share_item" },
], ],
tasks: [ tasks: [
{ label: 'New task', action: 'create_task' }, { label: "New task", action: "create_task" },
{ label: 'Due today', action: 'show_due_today' }, { label: "Due today", action: "show_due_today" },
{ label: 'Summary', action: 'tasks_summary' }, { label: "Summary", action: "tasks_summary" },
{ label: 'Priorities', action: 'show_priorities' } { label: "Priorities", action: "show_priorities" },
], ],
mail: [ mail: [
{ label: 'Compose', action: 'compose_email' }, { label: "Compose", action: "compose_email" },
{ label: 'Unread', action: 'show_unread' }, { label: "Unread", action: "show_unread" },
{ label: 'Search', action: 'search_mail' }, { label: "Search", action: "search_mail" },
{ label: 'Summary', action: 'mail_summary' } { label: "Summary", action: "mail_summary" },
], ],
calendar: [ calendar: [
{ label: 'New event', action: 'create_event' }, { label: "New event", action: "create_event" },
{ label: 'Today', action: 'show_today' }, { label: "Today", action: "show_today" },
{ label: 'This week', action: 'show_week' }, { label: "This week", action: "show_week" },
{ label: 'Find time', action: 'find_free_time' } { label: "Find time", action: "find_free_time" },
], ],
meet: [ meet: [
{ label: 'Start call', action: 'start_meeting' }, { label: "Start call", action: "start_meeting" },
{ label: 'Schedule', action: 'schedule_meeting' }, { label: "Schedule", action: "schedule_meeting" },
{ label: 'Join', action: 'join_meeting' } { label: "Join", action: "join_meeting" },
], ],
paper: [ paper: [
{ label: 'New doc', action: 'create_document' }, { label: "New doc", action: "create_document" },
{ label: 'Templates', action: 'show_templates' }, { label: "Templates", action: "show_templates" },
{ label: 'Recent', action: 'show_recent' } { label: "Recent", action: "show_recent" },
], ],
research: [ research: [
{ label: 'New search', action: 'new_research' }, { label: "New search", action: "new_research" },
{ label: 'Sources', action: 'show_sources' }, { label: "Sources", action: "show_sources" },
{ label: 'Citations', action: 'generate_citations' } { label: "Citations", action: "generate_citations" },
], ],
sources: [ sources: [
{ label: 'Add source', action: 'add_source' }, { label: "Add source", action: "add_source" },
{ label: 'Import', action: 'import_sources' }, { label: "Import", action: "import_sources" },
{ label: 'Categories', action: 'show_categories' } { label: "Categories", action: "show_categories" },
], ],
analytics: [ analytics: [
{ label: 'Dashboard', action: 'show_dashboard' }, { label: "Dashboard", action: "show_dashboard" },
{ label: 'Reports', action: 'show_reports' }, { label: "Reports", action: "show_reports" },
{ label: 'Export', action: 'export_data' } { label: "Export", action: "export_data" },
], ],
admin: [ admin: [
{ label: 'Users', action: 'manage_users' }, { label: "Users", action: "manage_users" },
{ label: 'Settings', action: 'show_settings' }, { label: "Settings", action: "show_settings" },
{ label: 'Logs', action: 'show_logs' } { label: "Logs", action: "show_logs" },
], ],
monitoring: [ monitoring: [
{ label: 'Status', action: 'show_status' }, { label: "Status", action: "show_status" },
{ label: 'Alerts', action: 'show_alerts' }, { label: "Alerts", action: "show_alerts" },
{ label: 'Metrics', action: 'show_metrics' } { label: "Metrics", action: "show_metrics" },
], ],
default: [ default: [
{ label: 'Help', action: 'show_help' }, { label: "Help", action: "show_help" },
{ label: 'Shortcuts', action: 'show_shortcuts' }, { label: "Shortcuts", action: "show_shortcuts" },
{ label: 'Settings', action: 'open_settings' } { label: "Settings", action: "open_settings" },
] ],
}; };
// Get current app from URL or hash // Get current app from URL or hash
function getCurrentApp() { function getCurrentApp() {
const hash = window.location.hash.replace('#', ''); const hash = window.location.hash.replace("#", "");
const path = window.location.pathname; const path = window.location.pathname;
if (hash) return hash; if (hash) return hash;
const match = path.match(/\/([a-z]+)\//); const match = path.match(/\/([a-z]+)\//);
return match ? match[1] : 'default'; return match ? match[1] : "default";
} }
// Update body data-app attribute // Update body data-app attribute
function updateCurrentApp() { function updateCurrentApp() {
const app = getCurrentApp(); const app = getCurrentApp();
document.body.setAttribute('data-app', app); document.body.setAttribute("data-app", app);
loadQuickActions(app); loadQuickActions(app);
} }
// Load quick actions for current app // Load quick actions for current app
function loadQuickActions(app) { function loadQuickActions(app) {
const container = document.getElementById('ai-quick-actions'); const container = document.getElementById("ai-quick-actions");
if (!container) return; if (!container) return;
const actions = aiQuickActions[app] || aiQuickActions.default; const actions = aiQuickActions[app] || aiQuickActions.default;
container.innerHTML = actions.map(a => container.innerHTML = actions
`<button class="quick-action-btn" onclick="handleQuickAction('${a.action}')">${a.label}</button>` .map(
).join(''); (a) =>
`<button class="quick-action-btn" onclick="handleQuickAction('${a.action}')">${a.label}</button>`,
)
.join("");
} }
// Handle quick action click // Handle quick action click
function handleQuickAction(action) { function handleQuickAction(action) {
const input = document.getElementById('ai-input'); const input = document.getElementById("ai-input");
const actionMessages = { const actionMessages = {
upload_file: 'Help me upload a file', upload_file: "Help me upload a file",
create_folder: 'Create a new folder', create_folder: "Create a new folder",
search_files: 'Search for files', search_files: "Search for files",
share_item: 'Help me share this item', share_item: "Help me share this item",
create_task: 'Create a new task', create_task: "Create a new task",
show_due_today: 'Show tasks due today', show_due_today: "Show tasks due today",
tasks_summary: 'Give me a summary of my tasks', tasks_summary: "Give me a summary of my tasks",
show_priorities: 'Show my priority tasks', show_priorities: "Show my priority tasks",
compose_email: 'Help me compose an email', compose_email: "Help me compose an email",
show_unread: 'Show unread emails', show_unread: "Show unread emails",
search_mail: 'Search my emails', search_mail: "Search my emails",
mail_summary: 'Summarize my inbox', mail_summary: "Summarize my inbox",
create_event: 'Create a calendar event', create_event: "Create a calendar event",
show_today: 'Show today\'s schedule', show_today: "Show today's schedule",
show_week: 'Show this week\'s events', show_week: "Show this week's events",
find_free_time: 'Find free time slots', find_free_time: "Find free time slots",
start_meeting: 'Start a new meeting', start_meeting: "Start a new meeting",
schedule_meeting: 'Schedule a meeting', schedule_meeting: "Schedule a meeting",
join_meeting: 'Join a meeting', join_meeting: "Join a meeting",
create_document: 'Create a new document', create_document: "Create a new document",
show_templates: 'Show document templates', show_templates: "Show document templates",
show_recent: 'Show recent documents', show_recent: "Show recent documents",
new_research: 'Start new research', new_research: "Start new research",
show_sources: 'Show my sources', show_sources: "Show my sources",
generate_citations: 'Generate citations', generate_citations: "Generate citations",
add_source: 'Add a new source', add_source: "Add a new source",
import_sources: 'Import sources', import_sources: "Import sources",
show_categories: 'Show categories', show_categories: "Show categories",
show_dashboard: 'Show analytics dashboard', show_dashboard: "Show analytics dashboard",
show_reports: 'Show reports', show_reports: "Show reports",
export_data: 'Export analytics data', export_data: "Export analytics data",
manage_users: 'Manage users', manage_users: "Manage users",
show_settings: 'Show admin settings', show_settings: "Show admin settings",
show_logs: 'Show system logs', show_logs: "Show system logs",
show_status: 'Show system status', show_status: "Show system status",
show_alerts: 'Show active alerts', show_alerts: "Show active alerts",
show_metrics: 'Show performance metrics', show_metrics: "Show performance metrics",
show_help: 'Help me get started', show_help: "Help me get started",
show_shortcuts: 'Show keyboard shortcuts', show_shortcuts: "Show keyboard shortcuts",
open_settings: 'Open settings' open_settings: "Open settings",
}; };
if (input && actionMessages[action]) { if (input && actionMessages[action]) {
@ -1333,34 +1246,38 @@
// Toggle AI panel // Toggle AI panel
function toggleAIPanel() { function toggleAIPanel() {
document.body.classList.toggle('ai-panel-collapsed'); document.body.classList.toggle("ai-panel-collapsed");
localStorage.setItem('ai-panel-collapsed', document.body.classList.contains('ai-panel-collapsed')); localStorage.setItem(
"ai-panel-collapsed",
document.body.classList.contains("ai-panel-collapsed"),
);
} }
// Send AI message // Send AI message
function sendAIMessage() { function sendAIMessage() {
const input = document.getElementById('ai-input'); const input = document.getElementById("ai-input");
const messagesContainer = document.getElementById('ai-messages'); const messagesContainer =
document.getElementById("ai-messages");
const message = input?.value?.trim(); const message = input?.value?.trim();
if (!message || !messagesContainer) return; if (!message || !messagesContainer) return;
// Add user message // Add user message
const userMsg = document.createElement('div'); const userMsg = document.createElement("div");
userMsg.className = 'ai-message user'; userMsg.className = "ai-message user";
userMsg.innerHTML = `<div class="ai-message-bubble">${escapeHtml(message)}</div>`; userMsg.innerHTML = `<div class="ai-message-bubble">${escapeHtml(message)}</div>`;
messagesContainer.appendChild(userMsg); messagesContainer.appendChild(userMsg);
// Clear input // Clear input
input.value = ''; input.value = "";
// Scroll to bottom // Scroll to bottom
messagesContainer.scrollTop = messagesContainer.scrollHeight; messagesContainer.scrollTop = messagesContainer.scrollHeight;
// Show typing indicator // Show typing indicator
const typing = document.createElement('div'); const typing = document.createElement("div");
typing.className = 'ai-message assistant'; typing.className = "ai-message assistant";
typing.id = 'ai-typing'; typing.id = "ai-typing";
typing.innerHTML = `<div class="ai-typing-indicator"><span></span><span></span><span></span></div>`; typing.innerHTML = `<div class="ai-typing-indicator"><span></span><span></span><span></span></div>`;
messagesContainer.appendChild(typing); messagesContainer.appendChild(typing);
messagesContainer.scrollTop = messagesContainer.scrollHeight; messagesContainer.scrollTop = messagesContainer.scrollHeight;
@ -1368,42 +1285,44 @@
// Simulate AI response (replace with actual API call) // Simulate AI response (replace with actual API call)
setTimeout(() => { setTimeout(() => {
typing.remove(); typing.remove();
const aiMsg = document.createElement('div'); const aiMsg = document.createElement("div");
aiMsg.className = 'ai-message assistant'; aiMsg.className = "ai-message assistant";
aiMsg.innerHTML = `<div class="ai-message-bubble">Entendi! Estou processando sua solicitação: "${escapeHtml(message)}". Como posso ajudar mais?</div>`; aiMsg.innerHTML = `<div class="ai-message-bubble">Entendi! Estou processando sua solicitação: "${escapeHtml(message)}". Como posso ajudar mais?</div>`;
messagesContainer.appendChild(aiMsg); messagesContainer.appendChild(aiMsg);
messagesContainer.scrollTop = messagesContainer.scrollHeight; messagesContainer.scrollTop =
messagesContainer.scrollHeight;
}, 1500); }, 1500);
} }
// Escape HTML // Escape HTML
function escapeHtml(text) { function escapeHtml(text) {
const div = document.createElement('div'); const div = document.createElement("div");
div.textContent = text; div.textContent = text;
return div.innerHTML; return div.innerHTML;
} }
// Restore AI panel state on load // Restore AI panel state on load
function initAIPanel() { function initAIPanel() {
const collapsed = localStorage.getItem('ai-panel-collapsed') === 'true'; const collapsed =
localStorage.getItem("ai-panel-collapsed") === "true";
if (collapsed) { if (collapsed) {
document.body.classList.add('ai-panel-collapsed'); document.body.classList.add("ai-panel-collapsed");
} }
updateCurrentApp(); updateCurrentApp();
} }
// Initialize on DOM ready // Initialize on DOM ready
document.addEventListener('DOMContentLoaded', initAIPanel); document.addEventListener("DOMContentLoaded", initAIPanel);
// Update app on navigation // Update app on navigation
document.body.addEventListener('htmx:afterSwap', function(e) { document.body.addEventListener("htmx:afterSwap", function (e) {
if (e.detail.target.id === 'main-content') { if (e.detail.target.id === "main-content") {
updateCurrentApp(); updateCurrentApp();
} }
}); });
// Also track hash changes // Also track hash changes
window.addEventListener('hashchange', updateCurrentApp); window.addEventListener("hashchange", updateCurrentApp);
</script> </script>
{% block scripts %}{% endblock %} {% block scripts %}{% endblock %}

View file

@ -3,9 +3,10 @@
/* Calendar Container */ /* Calendar Container */
.calendar-container { .calendar-container {
display: flex; display: flex;
height: calc(100vh - 60px); height: 100%;
background: var(--background); min-height: 0;
color: var(--foreground); background: var(--background, var(--bg-primary, #0a0a0f));
color: var(--foreground, var(--text-primary, #ffffff));
} }
/* Sidebar */ /* Sidebar */
@ -135,7 +136,7 @@
} }
.mini-day.has-events::after { .mini-day.has-events::after {
content: ''; content: "";
position: absolute; position: absolute;
bottom: 2px; bottom: 2px;
width: 4px; width: 4px;
@ -199,7 +200,7 @@
} }
.calendar-checkbox.checked::after { .calendar-checkbox.checked::after {
content: '✓'; content: "✓";
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
@ -562,11 +563,26 @@
cursor: pointer; cursor: pointer;
} }
.month-event.blue { background: rgba(59, 130, 246, 0.2); color: #3b82f6; } .month-event.blue {
.month-event.green { background: rgba(34, 197, 94, 0.2); color: #22c55e; } background: rgba(59, 130, 246, 0.2);
.month-event.red { background: rgba(239, 68, 68, 0.2); color: #ef4444; } color: #3b82f6;
.month-event.purple { background: rgba(168, 85, 247, 0.2); color: #a855f7; } }
.month-event.orange { background: rgba(249, 115, 22, 0.2); color: #f97316; } .month-event.green {
background: rgba(34, 197, 94, 0.2);
color: #22c55e;
}
.month-event.red {
background: rgba(239, 68, 68, 0.2);
color: #ef4444;
}
.month-event.purple {
background: rgba(168, 85, 247, 0.2);
color: #a855f7;
}
.month-event.orange {
background: rgba(249, 115, 22, 0.2);
color: #f97316;
}
.more-events { .more-events {
font-size: 11px; font-size: 11px;
@ -592,11 +608,26 @@
z-index: 5; z-index: 5;
} }
.calendar-event.blue { background: rgba(59, 130, 246, 0.9); color: white; } .calendar-event.blue {
.calendar-event.green { background: rgba(34, 197, 94, 0.9); color: white; } background: rgba(59, 130, 246, 0.9);
.calendar-event.red { background: rgba(239, 68, 68, 0.9); color: white; } color: white;
.calendar-event.purple { background: rgba(168, 85, 247, 0.9); color: white; } }
.calendar-event.orange { background: rgba(249, 115, 22, 0.9); color: white; } .calendar-event.green {
background: rgba(34, 197, 94, 0.9);
color: white;
}
.calendar-event.red {
background: rgba(239, 68, 68, 0.9);
color: white;
}
.calendar-event.purple {
background: rgba(168, 85, 247, 0.9);
color: white;
}
.calendar-event.orange {
background: rgba(249, 115, 22, 0.9);
color: white;
}
.event-title { .event-title {
font-weight: 500; font-weight: 500;
@ -622,7 +653,7 @@
} }
.current-time-indicator::before { .current-time-indicator::before {
content: ''; content: "";
position: absolute; position: absolute;
left: -5px; left: -5px;
top: -4px; top: -4px;
@ -782,7 +813,9 @@
.color-option input:checked + .color-dot { .color-option input:checked + .color-dot {
transform: scale(1.2); transform: scale(1.2);
box-shadow: 0 0 0 3px var(--background), 0 0 0 5px currentColor; box-shadow:
0 0 0 3px var(--background),
0 0 0 5px currentColor;
} }
.form-actions { .form-actions {
@ -909,7 +942,9 @@
text-decoration: none; text-decoration: none;
border-radius: 8px; border-radius: 8px;
cursor: pointer; cursor: pointer;
transition: background 0.2s, border-color 0.2s; transition:
background 0.2s,
border-color 0.2s;
} }
.ical-btn:hover { .ical-btn:hover {

View file

@ -14,7 +14,7 @@
<!-- Quick Actions --> <!-- Quick Actions -->
<div class="sidebar-actions"> <div class="sidebar-actions">
<button class="btn-primary-full" id="new-event-btn" <button class="btn-primary-full" id="new-event-btn"
hx-get="/api/calendar/event/new" hx-get="/ui/calendar/event/new"
hx-target="#event-modal-content" hx-target="#event-modal-content"
hx-swap="innerHTML"> hx-swap="innerHTML">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
@ -55,7 +55,7 @@
<div class="section-header"> <div class="section-header">
<h3>My Calendars</h3> <h3>My Calendars</h3>
<button class="btn-icon-sm" title="Add Calendar" <button class="btn-icon-sm" title="Add Calendar"
hx-get="/api/calendar/new" hx-get="/ui/calendar/new"
hx-target="#calendar-modal-content"> hx-target="#calendar-modal-content">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<line x1="12" y1="5" x2="12" y2="19"></line> <line x1="12" y1="5" x2="12" y2="19"></line>
@ -64,7 +64,7 @@
</button> </button>
</div> </div>
<div class="calendars-list" id="calendars-list" <div class="calendars-list" id="calendars-list"
hx-get="/api/calendar/list" hx-get="/ui/calendar/list"
hx-trigger="load" hx-trigger="load"
hx-swap="innerHTML"> hx-swap="innerHTML">
<!-- Calendars loaded here --> <!-- Calendars loaded here -->
@ -75,7 +75,7 @@
<div class="sidebar-section"> <div class="sidebar-section">
<h3>Upcoming</h3> <h3>Upcoming</h3>
<div class="upcoming-events" id="upcoming-events" <div class="upcoming-events" id="upcoming-events"
hx-get="/api/calendar/upcoming" hx-get="/ui/calendar/upcoming"
hx-trigger="load" hx-trigger="load"
hx-swap="innerHTML"> hx-swap="innerHTML">
<!-- Upcoming events loaded here --> <!-- Upcoming events loaded here -->
@ -423,7 +423,7 @@
<div class="form-group"> <div class="form-group">
<label>Import to calendar</label> <label>Import to calendar</label>
<select name="calendar_id" <select name="calendar_id"
hx-get="/api/calendar/list" hx-get="/ui/calendar/list"
hx-trigger="load" hx-trigger="load"
hx-swap="innerHTML"> hx-swap="innerHTML">
<option value="">Default Calendar</option> <option value="">Default Calendar</option>
@ -1692,6 +1692,8 @@
function updateCurrentTimeIndicator() { function updateCurrentTimeIndicator() {
const indicator = document.getElementById('current-time-indicator'); const indicator = document.getElementById('current-time-indicator');
if (!indicator) return; // Guard against null element
const now = new Date(); const now = new Date();
const minutes = now.getHours() * 60 + now.getMinutes(); const minutes = now.getHours() * 60 + now.getMinutes();
const top = (minutes / 60) * 48; // 48px per hour const top = (minutes / 60) * 48; // 48px per hour

View file

@ -41,8 +41,15 @@
} }
@keyframes pulse { @keyframes pulse {
0%, 100% { opacity: 1; transform: scale(1); } 0%,
50% { opacity: 0.5; transform: scale(1.2); } 100% {
opacity: 1;
transform: scale(1);
}
50% {
opacity: 0.5;
transform: scale(1.2);
}
} }
/* Messages Area */ /* Messages Area */
@ -62,8 +69,14 @@
} }
@keyframes slideIn { @keyframes slideIn {
from { opacity: 0; transform: translateY(10px); } from {
to { opacity: 1; transform: translateY(0); } opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
} }
.message.user { .message.user {
@ -113,7 +126,7 @@
background: rgba(0, 0, 0, 0.1); background: rgba(0, 0, 0, 0.1);
padding: 2px 6px; padding: 2px 6px;
border-radius: 4px; border-radius: 4px;
font-family: 'Monaco', 'Menlo', monospace; font-family: "Monaco", "Menlo", monospace;
font-size: 13px; font-size: 13px;
} }
@ -165,25 +178,44 @@ footer {
/* Input Container */ /* Input Container */
.input-container { .input-container {
display: flex; display: flex;
gap: 8px; gap: 12px;
align-items: center; align-items: center;
padding: 8px 12px;
background: var(--surface, var(--secondary-bg, #f9fafb));
border: 1px solid var(--border, var(--border-color, #e5e7eb));
border-radius: 28px;
}
[data-theme="dark"] .input-container,
[data-theme="sentient"] .input-container {
background: var(--surface, #1a1a24);
border-color: var(--border, #2a2a3a);
}
.input-container:focus-within {
border-color: var(--accent, var(--accent-color, #3b82f6));
box-shadow: 0 0 0 3px var(--accent-glow, rgba(59, 130, 246, 0.1));
} }
#messageInput { #messageInput {
flex: 1; flex: 1;
padding: 12px 16px; padding: 8px 12px;
border-radius: 24px; border-radius: 20px;
border: 1px solid var(--border-color, #e5e7eb); border: none;
background: var(--secondary-bg, #f9fafb); background: transparent;
color: var(--text-primary, #1f2937); color: var(--text-primary, #1f2937);
font-size: 14px; font-size: 15px;
outline: none; outline: none;
transition: all 0.2s; transition: all 0.2s;
} }
[data-theme="dark"] #messageInput,
[data-theme="sentient"] #messageInput {
color: var(--text-primary, #ffffff);
}
#messageInput:focus { #messageInput:focus {
border-color: var(--accent-color, #3b82f6); outline: none;
box-shadow: 0 0 0 3px var(--accent-light, rgba(59, 130, 246, 0.1));
} }
#messageInput::placeholder { #messageInput::placeholder {
@ -192,24 +224,41 @@ footer {
#voiceBtn, #voiceBtn,
#sendBtn { #sendBtn {
width: 40px; width: 44px;
height: 40px; height: 44px;
min-width: 44px;
min-height: 44px;
border-radius: 50%; border-radius: 50%;
border: none; border: none;
background: var(--accent-color, #3b82f6); background: var(--accent, var(--accent-color, #3b82f6));
color: white; color: var(--accent-foreground, white);
cursor: pointer; cursor: pointer;
display: flex; display: flex !important;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
font-size: 16px; font-size: 18px;
transition: all 0.2s; transition: all 0.2s;
flex-shrink: 0;
}
#sendBtn {
background: var(--accent, var(--accent-color, #3b82f6));
}
#voiceBtn {
background: var(--surface, var(--secondary-bg, #f9fafb));
color: var(--text-primary, #374151);
border: 1px solid var(--border, var(--border-color, #e5e7eb));
} }
#voiceBtn:hover, #voiceBtn:hover,
#sendBtn:hover { #sendBtn:hover {
transform: scale(1.05); transform: scale(1.05);
background: var(--accent-hover, #2563eb); opacity: 0.9;
}
#sendBtn:hover {
background: var(--accent-hover, var(--accent, #2563eb));
} }
#voiceBtn:active, #voiceBtn:active,
@ -217,6 +266,20 @@ footer {
transform: scale(0.95); transform: scale(0.95);
} }
/* Dark/Sentient theme overrides */
[data-theme="dark"] #sendBtn,
[data-theme="sentient"] #sendBtn {
background: var(--accent, #d4f505);
color: #000000;
}
[data-theme="dark"] #voiceBtn,
[data-theme="sentient"] #voiceBtn {
background: var(--surface, #1a1a24);
color: var(--text-primary, #ffffff);
border-color: var(--border, #2a2a3a);
}
/* Scroll to Bottom Button */ /* Scroll to Bottom Button */
.scroll-to-bottom { .scroll-to-bottom {
position: fixed; position: fixed;
@ -257,7 +320,6 @@ footer {
} }
} }
/* Projector Overlay */ /* Projector Overlay */
.projector-overlay { .projector-overlay {
position: fixed; position: fixed;
@ -278,8 +340,12 @@ footer {
} }
@keyframes fadeIn { @keyframes fadeIn {
from { opacity: 0; } from {
to { opacity: 1; } opacity: 0;
}
to {
opacity: 1;
}
} }
/* Container */ /* Container */
@ -394,7 +460,9 @@ footer {
} }
@keyframes spin { @keyframes spin {
to { transform: rotate(360deg); } to {
transform: rotate(360deg);
}
} }
/* Video Player */ /* Video Player */
@ -445,7 +513,7 @@ footer {
.projector-code pre { .projector-code pre {
margin: 0; margin: 0;
font-family: 'Fira Code', 'Monaco', 'Consolas', monospace; font-family: "Fira Code", "Monaco", "Consolas", monospace;
font-size: 14px; font-size: 14px;
line-height: 1.5; line-height: 1.5;
color: #d4d4d4; color: #d4d4d4;

View file

@ -178,6 +178,86 @@
} }
} }
/* ============================================ */
/* SENTIENT THEME VARIABLES */
/* Dark background with neon lime/green accents */
/* ============================================ */
:root {
/* Sentient Core Colors - used by tasks, attendant, and other apps */
--sentient-accent: #d4f505;
--sentient-accent-hover: #bfdd04;
--sentient-accent-light: rgba(212, 245, 5, 0.15);
--sentient-accent-glow: rgba(212, 245, 5, 0.3);
/* Sentient Background Hierarchy */
--sentient-bg-primary: #0a0a0a;
--sentient-bg-secondary: #111111;
--sentient-bg-tertiary: #161616;
--sentient-bg-elevated: #1a1a1a;
--sentient-bg-hover: #1e1e1e;
--sentient-bg-active: #252525;
/* Sentient Borders */
--sentient-border: #2a2a2a;
--sentient-border-light: #222222;
--sentient-border-accent: rgba(212, 245, 5, 0.3);
/* Sentient Text Colors */
--sentient-text-primary: #ffffff;
--sentient-text-secondary: #888888;
--sentient-text-tertiary: #666666;
--sentient-text-muted: #444444;
--sentient-text-accent: #d4f505;
/* Sentient Status Colors */
--sentient-success: #22c55e;
--sentient-success-bg: rgba(34, 197, 94, 0.15);
--sentient-warning: #f59e0b;
--sentient-warning-bg: rgba(245, 158, 11, 0.15);
--sentient-error: #ef4444;
--sentient-error-bg: rgba(239, 68, 68, 0.15);
--sentient-info: #3b82f6;
--sentient-info-bg: rgba(59, 130, 246, 0.15);
/* Sentient Shadows */
--sentient-shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.4);
--sentient-shadow-md: 0 4px 12px rgba(0, 0, 0, 0.5);
--sentient-shadow-lg: 0 8px 24px rgba(0, 0, 0, 0.6);
--sentient-shadow-accent: 0 0 20px rgba(212, 245, 5, 0.2);
--sentient-shadow-glow: 0 0 30px rgba(212, 245, 5, 0.15);
/* Sentient Typography */
--sentient-font-family:
"Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
sans-serif;
/* Sentient Border Radius */
--sentient-radius-sm: 6px;
--sentient-radius-md: 8px;
--sentient-radius-lg: 12px;
--sentient-radius-xl: 16px;
--sentient-radius-full: 9999px;
}
/* Apply sentient variables when theme is active */
[data-theme="sentient"] {
--primary-bg: var(--sentient-bg-primary);
--primary-fg: var(--sentient-text-primary);
--secondary-bg: var(--sentient-bg-secondary);
--secondary-fg: var(--sentient-text-secondary);
--accent-color: var(--sentient-accent);
--accent-hover: var(--sentient-accent-hover);
--accent-light: var(--sentient-accent-light);
--border-color: var(--sentient-border);
--text-primary: var(--sentient-text-primary);
--text-secondary: var(--sentient-text-secondary);
--text-tertiary: var(--sentient-text-tertiary);
--success-color: var(--sentient-success);
--warning-color: var(--sentient-warning);
--error-color: var(--sentient-error);
--info-color: var(--sentient-info);
}
/* ============================================ */ /* ============================================ */
/* GLOBAL RESETS */ /* GLOBAL RESETS */
/* ============================================ */ /* ============================================ */
@ -216,10 +296,11 @@ body {
/* LAYOUT STRUCTURE */ /* LAYOUT STRUCTURE */
/* ============================================ */ /* ============================================ */
#main-content { #main-content {
height: 100vh; height: calc(100vh - 64px);
width: 100vw; width: 100%;
overflow: hidden; overflow: hidden;
position: relative; position: relative;
margin-top: 64px;
} }
.section { .section {

File diff suppressed because it is too large Load diff

View file

@ -83,6 +83,22 @@
--border: #5c3a1e; --border: #5c3a1e;
} }
[data-theme="sentient"] {
--primary: #d4f505;
--primary-hover: #bfdd04;
--primary-light: rgba(212, 245, 5, 0.15);
--bg: #0a0a0a;
--surface: #161616;
--surface-hover: #1e1e1e;
--border: #2a2a2a;
--text: #ffffff;
--text-secondary: #888888;
--success: #22c55e;
--warning: #f59e0b;
--error: #ef4444;
--info: #3b82f6;
}
/* ============================================ */ /* ============================================ */
/* BASE RESETS */ /* BASE RESETS */
/* ============================================ */ /* ============================================ */
@ -94,7 +110,9 @@
} }
body { body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; font-family:
-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue",
Arial, sans-serif;
background: var(--bg); background: var(--bg);
color: var(--text); color: var(--text);
min-height: 100vh; min-height: 100vh;
@ -167,7 +185,9 @@ body {
color: var(--text-secondary); color: var(--text-secondary);
border-radius: 8px; border-radius: 8px;
cursor: pointer; cursor: pointer;
transition: background 0.2s, color 0.2s; transition:
background 0.2s,
color 0.2s;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
@ -314,9 +334,51 @@ body {
} }
/* ============================================ */ /* ============================================ */
/* THEME SELECTOR */ /* THEME SELECTOR DROPDOWN */
/* ============================================ */ /* ============================================ */
.theme-dropdown {
width: 100%;
padding: 10px 14px;
background: var(--surface, #161616);
border: 1px solid var(--border, #2a2a2a);
border-radius: 8px;
color: var(--text, #ffffff);
font-size: 14px;
font-family: inherit;
cursor: pointer;
transition: all 0.2s ease;
appearance: none;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%23888888' stroke-width='2'%3E%3Cpolyline points='6 9 12 15 18 9'%3E%3C/polyline%3E%3C/svg%3E");
background-repeat: no-repeat;
background-position: right 12px center;
padding-right: 36px;
}
.theme-dropdown:hover {
border-color: var(--text-secondary, #888888);
}
.theme-dropdown:focus {
outline: none;
border-color: var(--primary, #d4f505);
box-shadow: 0 0 0 3px rgba(212, 245, 5, 0.15);
}
.theme-dropdown option {
background: var(--surface, #161616);
color: var(--text, #ffffff);
padding: 8px;
}
.theme-dropdown optgroup {
background: var(--bg, #0a0a0a);
color: var(--text-secondary, #888888);
font-weight: 600;
font-style: normal;
}
/* Legacy theme-grid support (if needed) */
.theme-grid { .theme-grid {
display: grid; display: grid;
grid-template-columns: repeat(3, 1fr); grid-template-columns: repeat(3, 1fr);
@ -385,35 +447,139 @@ body {
} }
/* Theme Preview Colors */ /* Theme Preview Colors */
.theme-dark .theme-option-inner { background: #0f172a; color: #f8fafc; } .theme-dark .theme-option-inner {
.theme-dark .theme-option-header { background: #1e293b; } background: #0f172a;
.theme-dark .theme-option-dot { background: #3b82f6; } color: #f8fafc;
.theme-dark .theme-option-line { background: #334155; } }
.theme-dark .theme-option-header {
background: #1e293b;
}
.theme-dark .theme-option-dot {
background: #3b82f6;
}
.theme-dark .theme-option-line {
background: #334155;
}
.theme-light .theme-option-inner { background: #f8fafc; color: #1e293b; } .theme-light .theme-option-inner {
.theme-light .theme-option-header { background: #ffffff; } background: #f8fafc;
.theme-light .theme-option-dot { background: #3b82f6; } color: #1e293b;
.theme-light .theme-option-line { background: #e2e8f0; } }
.theme-light .theme-option-header {
background: #ffffff;
}
.theme-light .theme-option-dot {
background: #3b82f6;
}
.theme-light .theme-option-line {
background: #e2e8f0;
}
.theme-blue .theme-option-inner { background: #0c1929; color: #f8fafc; } .theme-blue .theme-option-inner {
.theme-blue .theme-option-header { background: #1a2f47; } background: #0c1929;
.theme-blue .theme-option-dot { background: #0ea5e9; } color: #f8fafc;
.theme-blue .theme-option-line { background: #2d4a6f; } }
.theme-blue .theme-option-header {
background: #1a2f47;
}
.theme-blue .theme-option-dot {
background: #0ea5e9;
}
.theme-blue .theme-option-line {
background: #2d4a6f;
}
.theme-purple .theme-option-inner { background: #1a0a2e; color: #f8fafc; } .theme-purple .theme-option-inner {
.theme-purple .theme-option-header { background: #2d1b4e; } background: #1a0a2e;
.theme-purple .theme-option-dot { background: #a855f7; } color: #f8fafc;
.theme-purple .theme-option-line { background: #4c2f7e; } }
.theme-purple .theme-option-header {
background: #2d1b4e;
}
.theme-purple .theme-option-dot {
background: #a855f7;
}
.theme-purple .theme-option-line {
background: #4c2f7e;
}
.theme-green .theme-option-inner { background: #0a1f15; color: #f8fafc; } .theme-green .theme-option-inner {
.theme-green .theme-option-header { background: #14332a; } background: #0a1f15;
.theme-green .theme-option-dot { background: #22c55e; } color: #f8fafc;
.theme-green .theme-option-line { background: #28604f; } }
.theme-green .theme-option-header {
background: #14332a;
}
.theme-green .theme-option-dot {
background: #22c55e;
}
.theme-green .theme-option-line {
background: #28604f;
}
.theme-orange .theme-option-inner { background: #1a1008; color: #f8fafc; } .theme-orange .theme-option-inner {
.theme-orange .theme-option-header { background: #2d1c0f; } background: #1a1008;
.theme-orange .theme-option-dot { background: #f97316; } color: #f8fafc;
.theme-orange .theme-option-line { background: #5c3a1e; } }
.theme-orange .theme-option-header {
background: #2d1c0f;
}
.theme-orange .theme-option-dot {
background: #f97316;
}
.theme-orange .theme-option-line {
background: #5c3a1e;
}
.theme-sentient .theme-option-inner {
background: #0a0a0a;
color: #ffffff;
}
.theme-sentient .theme-option-header {
background: #161616;
}
.theme-sentient .theme-option-dot {
background: #d4f505;
}
.theme-sentient .theme-option-line {
background: #2a2a2a;
}
/* ============================================ */
/* SETTINGS PANEL */
/* ============================================ */
.settings-panel {
position: absolute;
top: calc(100% + 8px);
right: 0;
width: 280px;
background: var(--surface, #161616);
border: 1px solid var(--border, #2a2a2a);
border-radius: 12px;
padding: 16px;
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.4);
opacity: 0;
visibility: hidden;
transform: translateY(-10px);
transition: all 0.2s ease;
z-index: 1100;
}
.settings-panel.show {
opacity: 1;
visibility: visible;
transform: translateY(0);
}
.settings-panel-title {
font-size: 12px;
font-weight: 600;
color: var(--text-secondary, #888888);
text-transform: uppercase;
letter-spacing: 0.5px;
margin-bottom: 12px;
}
/* ============================================ */ /* ============================================ */
/* SETTINGS SHORTCUTS */ /* SETTINGS SHORTCUTS */
@ -647,7 +813,12 @@ body {
/* ============================================ */ /* ============================================ */
.skeleton { .skeleton {
background: linear-gradient(90deg, var(--border) 25%, var(--surface-hover) 50%, var(--border) 75%); background: linear-gradient(
90deg,
var(--border) 25%,
var(--surface-hover) 50%,
var(--border) 75%
);
background-size: 200% 100%; background-size: 200% 100%;
animation: skeleton-loading 1.5s infinite; animation: skeleton-loading 1.5s infinite;
border-radius: 4px; border-radius: 4px;
@ -668,8 +839,12 @@ body {
} }
@keyframes skeleton-loading { @keyframes skeleton-loading {
0% { background-position: 200% 0; } 0% {
100% { background-position: -200% 0; } background-position: 200% 0;
}
100% {
background-position: -200% 0;
}
} }
/* ============================================ */ /* ============================================ */
@ -687,7 +862,9 @@ body {
} }
@keyframes spin { @keyframes spin {
to { transform: rotate(360deg); } to {
transform: rotate(360deg);
}
} }
/* ============================================ */ /* ============================================ */
@ -717,10 +894,18 @@ body {
animation: slideIn 0.3s ease; animation: slideIn 0.3s ease;
} }
.notification.success { border-left: 3px solid var(--success); } .notification.success {
.notification.error { border-left: 3px solid var(--error); } border-left: 3px solid var(--success);
.notification.warning { border-left: 3px solid var(--warning); } }
.notification.info { border-left: 3px solid var(--info); } .notification.error {
border-left: 3px solid var(--error);
}
.notification.warning {
border-left: 3px solid var(--warning);
}
.notification.info {
border-left: 3px solid var(--info);
}
@keyframes slideIn { @keyframes slideIn {
from { from {

View file

@ -413,18 +413,44 @@ select:focus {
/* MODALS */ /* MODALS */
/* ============================================ */ /* ============================================ */
.modal { .modal-overlay {
position: fixed; position: fixed;
inset: 0; inset: 0;
background: hsla(var(--foreground) / 0.5); background: rgba(0, 0, 0, 0.7);
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
z-index: 200; z-index: 1100;
padding: 16px; padding: 16px;
animation: modalFadeIn 0.2s ease; opacity: 0;
visibility: hidden;
transition: all 0.2s ease;
} }
.modal-overlay.show {
opacity: 1;
visibility: visible;
}
.modal {
width: 100%;
max-width: 520px;
max-height: calc(100vh - 32px);
background: var(--surface, #161616);
border: 1px solid var(--border, #2a2a2a);
border-radius: 16px;
overflow: hidden;
display: flex;
flex-direction: column;
transform: scale(0.95) translateY(-10px);
transition: transform 0.2s ease;
}
.modal-overlay.show .modal {
transform: scale(1) translateY(0);
}
/* Legacy support */
.modal.hidden { .modal.hidden {
display: none; display: none;
} }
@ -442,23 +468,12 @@ select:focus {
width: 100%; width: 100%;
max-width: 480px; max-width: 480px;
max-height: calc(100vh - 32px); max-height: calc(100vh - 32px);
background: hsl(var(--card)); background: var(--surface, #161616);
border: 1px solid var(--border, #2a2a2a);
border-radius: 12px; border-radius: 12px;
overflow: hidden; overflow: hidden;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
animation: modalSlideIn 0.2s ease;
}
@keyframes modalSlideIn {
from {
opacity: 0;
transform: scale(0.95) translateY(-10px);
}
to {
opacity: 1;
transform: scale(1) translateY(0);
}
} }
.modal-sm .modal-content { .modal-sm .modal-content {
@ -482,28 +497,97 @@ select:focus {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
padding: 16px 20px; padding: 20px 24px;
border-bottom: 1px solid hsl(var(--border)); border-bottom: 1px solid var(--border, #2a2a2a);
} }
.modal-header h3 { .modal-header h3 {
margin: 0;
font-size: 18px; font-size: 18px;
font-weight: 600; font-weight: 600;
color: var(--text, #ffffff);
margin: 0;
}
.modal-close {
display: flex;
align-items: center;
justify-content: center;
width: 32px;
height: 32px;
background: transparent;
border: none;
border-radius: 6px;
color: var(--text-secondary, #888888);
cursor: pointer;
transition: all 0.2s ease;
}
.modal-close:hover {
background: var(--surface-hover, #1e1e1e);
color: var(--text, #ffffff);
} }
.modal-body { .modal-body {
flex: 1; flex: 1;
padding: 20px;
overflow-y: auto; overflow-y: auto;
padding: 24px;
} }
.modal-footer { .modal-footer {
display: flex; display: flex;
justify-content: flex-end; justify-content: flex-end;
gap: 12px; gap: 12px;
padding: 16px 20px; padding: 16px 24px;
border-top: 1px solid hsl(var(--border)); border-top: 1px solid var(--border, #2a2a2a);
}
/* Form styles within modals */
.form-group {
margin-bottom: 20px;
}
.form-row {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 16px;
}
.form-label {
display: block;
font-size: 14px;
font-weight: 500;
color: var(--text-secondary, #888888);
margin-bottom: 8px;
}
.form-input {
width: 100%;
padding: 12px 14px;
background: var(--bg, #0a0a0a);
border: 1px solid var(--border, #2a2a2a);
border-radius: 8px;
color: var(--text, #ffffff);
font-size: 14px;
transition: all 0.2s ease;
}
.form-input:focus {
outline: none;
border-color: var(--accent, #d4f505);
box-shadow: 0 0 0 3px rgba(212, 245, 5, 0.1);
}
.form-input::placeholder {
color: var(--text-tertiary, #666666);
}
textarea.form-input {
resize: vertical;
min-height: 80px;
}
select.form-input {
cursor: pointer;
} }
/* ============================================ */ /* ============================================ */
@ -940,7 +1024,7 @@ select:focus {
.divider-text::before, .divider-text::before,
.divider-text::after { .divider-text::after {
content: ''; content: "";
flex: 1; flex: 1;
height: 1px; height: 1px;
background: hsl(var(--border)); background: hsl(var(--border));
@ -1019,28 +1103,70 @@ select:focus {
justify-content: space-between; justify-content: space-between;
} }
.gap-1 { gap: 4px; } .gap-1 {
.gap-2 { gap: 8px; } gap: 4px;
.gap-3 { gap: 12px; } }
.gap-4 { gap: 16px; } .gap-2 {
.gap-5 { gap: 20px; } gap: 8px;
}
.gap-3 {
gap: 12px;
}
.gap-4 {
gap: 16px;
}
.gap-5 {
gap: 20px;
}
.p-1 { padding: 4px; } .p-1 {
.p-2 { padding: 8px; } padding: 4px;
.p-3 { padding: 12px; } }
.p-4 { padding: 16px; } .p-2 {
.p-5 { padding: 20px; } padding: 8px;
}
.p-3 {
padding: 12px;
}
.p-4 {
padding: 16px;
}
.p-5 {
padding: 20px;
}
.m-1 { margin: 4px; } .m-1 {
.m-2 { margin: 8px; } margin: 4px;
.m-3 { margin: 12px; } }
.m-4 { margin: 16px; } .m-2 {
.m-5 { margin: 20px; } margin: 8px;
}
.m-3 {
margin: 12px;
}
.m-4 {
margin: 16px;
}
.m-5 {
margin: 20px;
}
.rounded { border-radius: 6px; } .rounded {
.rounded-lg { border-radius: 12px; } border-radius: 6px;
.rounded-full { border-radius: 9999px; } }
.rounded-lg {
border-radius: 12px;
}
.rounded-full {
border-radius: 9999px;
}
.shadow-sm { box-shadow: 0 1px 2px hsla(var(--foreground) / 0.05); } .shadow-sm {
.shadow { box-shadow: 0 2px 8px hsla(var(--foreground) / 0.08); } box-shadow: 0 1px 2px hsla(var(--foreground) / 0.05);
.shadow-lg { box-shadow: 0 8px 24px hsla(var(--foreground) / 0.12); } }
.shadow {
box-shadow: 0 2px 8px hsla(var(--foreground) / 0.08);
}
.shadow-lg {
box-shadow: 0 8px 24px hsla(var(--foreground) / 0.12);
}

View file

@ -1,7 +1,13 @@
* { margin: 0; padding: 0; box-sizing: border-box; } * {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body { body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif; font-family:
-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu,
sans-serif;
background: #0f172a; background: #0f172a;
color: #e2e8f0; color: #e2e8f0;
height: 100vh; height: 100vh;
@ -49,8 +55,9 @@ nav a.active {
/* Main Content */ /* Main Content */
#main-content { #main-content {
height: calc(100vh - 60px); height: calc(100vh - 64px);
overflow: hidden; overflow: hidden;
position: relative;
} }
.content-section { .content-section {
@ -81,7 +88,9 @@ button:disabled {
} }
/* Utility */ /* Utility */
h1, h2, h3 { h1,
h2,
h3 {
margin-bottom: 1rem; margin-bottom: 1rem;
} }

View file

@ -0,0 +1,977 @@
/* General Bots Suite - Sentient Theme */
/* Dark background with neon lime/green accents */
/* Inspired by modern AI dashboard aesthetics */
[data-theme="sentient"] {
/* Core Colors */
--accent: #d4f505;
--accent-hover: #bfdd04;
--accent-light: rgba(212, 245, 5, 0.15);
--accent-glow: rgba(212, 245, 5, 0.3);
--accent-rgb: 212, 245, 5;
/* Background Hierarchy */
--bg: #0a0a0a;
--bg-secondary: #111111;
--surface: #161616;
--surface-hover: #1e1e1e;
--surface-active: #252525;
--surface-elevated: #1a1a1a;
/* Border Colors */
--border: #2a2a2a;
--border-light: #222222;
--border-accent: rgba(212, 245, 5, 0.3);
/* Text Colors */
--text: #ffffff;
--text-secondary: #888888;
--text-tertiary: #666666;
--text-muted: #444444;
--text-accent: #d4f505;
/* Status Colors */
--success: #22c55e;
--success-bg: rgba(34, 197, 94, 0.15);
--warning: #f59e0b;
--warning-bg: rgba(245, 158, 11, 0.15);
--error: #ef4444;
--error-bg: rgba(239, 68, 68, 0.15);
--info: #3b82f6;
--info-bg: rgba(59, 130, 246, 0.15);
/* Component Specific */
--header-bg: rgba(10, 10, 10, 0.95);
--header-border: #1e1e1e;
--sidebar-bg: #0f0f0f;
--sidebar-border: #1e1e1e;
--card-bg: #161616;
--card-border: #2a2a2a;
--card-hover-border: rgba(212, 245, 5, 0.4);
--input-bg: #1a1a1a;
--input-border: #2a2a2a;
--input-focus-border: #d4f505;
/* Shadows */
--shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.4);
--shadow-md: 0 4px 12px rgba(0, 0, 0, 0.5);
--shadow-lg: 0 8px 24px rgba(0, 0, 0, 0.6);
--shadow-accent: 0 0 20px rgba(212, 245, 5, 0.2);
--shadow-glow: 0 0 30px rgba(212, 245, 5, 0.15);
/* HSL Bridge Variables for compatibility */
--background: 0 0% 4%;
--foreground: 0 0% 100%;
--card: 0 0% 9%;
--card-foreground: 0 0% 100%;
--popover: 0 0% 7%;
--popover-foreground: 0 0% 100%;
--primary: 67 96% 49%;
--primary-foreground: 0 0% 0%;
--secondary: 0 0% 12%;
--secondary-foreground: 0 0% 100%;
--muted: 0 0% 15%;
--muted-foreground: 0 0% 53%;
--accent-hsl: 67 96% 49%;
--accent-foreground: 0 0% 0%;
--destructive: 0 84% 60%;
--destructive-foreground: 0 0% 100%;
--border-hsl: 0 0% 17%;
--input-hsl: 0 0% 17%;
--ring: 67 96% 49%;
}
/* ============================================ */
/* SENTIENT THEME - GLOBAL OVERRIDES */
/* ============================================ */
[data-theme="sentient"] body {
background: var(--bg);
color: var(--text);
}
/* Header Styling */
[data-theme="sentient"] .float-header {
background: var(--header-bg);
border-bottom: 1px solid var(--header-border);
backdrop-filter: blur(12px);
}
[data-theme="sentient"] .header-app-tabs .app-tab {
color: var(--text-secondary);
background: transparent;
border: 1px solid transparent;
border-radius: 8px;
transition: all 0.2s ease;
}
[data-theme="sentient"] .header-app-tabs .app-tab:hover {
color: var(--text);
background: var(--surface-hover);
}
[data-theme="sentient"] .header-app-tabs .app-tab.active {
color: #000000;
background: var(--accent);
border-color: var(--accent);
font-weight: 600;
}
[data-theme="sentient"] .header-app-tabs .app-tab.active svg {
stroke: #000000;
}
/* Search Bar */
[data-theme="sentient"] .header-search {
background: var(--surface);
border: 1px solid var(--border);
border-radius: 10px;
}
[data-theme="sentient"] .header-search:focus-within {
border-color: var(--accent);
box-shadow: var(--shadow-accent);
}
[data-theme="sentient"] .search-shortcut {
background: var(--surface-hover);
color: var(--text-tertiary);
border: 1px solid var(--border);
}
/* Icon Buttons */
[data-theme="sentient"] .icon-button {
color: var(--text-secondary);
background: transparent;
border: 1px solid transparent;
border-radius: 8px;
transition: all 0.2s ease;
}
[data-theme="sentient"] .icon-button:hover {
color: var(--text);
background: var(--surface-hover);
border-color: var(--border);
}
/* Notification Badge */
[data-theme="sentient"] .notification-badge {
background: var(--accent);
color: #000000;
font-weight: 700;
}
/* User Avatar */
[data-theme="sentient"] .user-avatar {
border: 2px solid var(--border);
}
[data-theme="sentient"] .user-avatar:hover {
border-color: var(--accent);
}
/* ============================================ */
/* CARDS & PANELS */
/* ============================================ */
[data-theme="sentient"] .card,
[data-theme="sentient"] .panel,
[data-theme="sentient"] .intent-card {
background: var(--card-bg);
border: 1px solid var(--card-border);
border-radius: 12px;
}
[data-theme="sentient"] .card:hover,
[data-theme="sentient"] .panel:hover,
[data-theme="sentient"] .intent-card:hover {
border-color: var(--card-hover-border);
}
/* ============================================ */
/* BUTTONS */
/* ============================================ */
[data-theme="sentient"] .btn-primary,
[data-theme="sentient"] .button-primary {
background: var(--accent);
color: #000000;
border: none;
font-weight: 600;
border-radius: 8px;
transition: all 0.2s ease;
}
[data-theme="sentient"] .btn-primary:hover,
[data-theme="sentient"] .button-primary:hover {
background: var(--accent-hover);
box-shadow: var(--shadow-accent);
}
[data-theme="sentient"] .btn-secondary,
[data-theme="sentient"] .button-secondary {
background: transparent;
color: var(--text);
border: 1px solid var(--border);
border-radius: 8px;
}
[data-theme="sentient"] .btn-secondary:hover,
[data-theme="sentient"] .button-secondary:hover {
background: var(--surface-hover);
border-color: var(--text-secondary);
}
[data-theme="sentient"] .btn-ghost {
background: transparent;
color: var(--text-secondary);
border: 1px solid transparent;
}
[data-theme="sentient"] .btn-ghost:hover {
background: var(--surface-hover);
color: var(--text);
}
/* ============================================ */
/* STATUS FILTERS / TABS */
/* ============================================ */
[data-theme="sentient"] .status-filter,
[data-theme="sentient"] .filter-tab {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 8px 16px;
background: transparent;
border: 1px solid var(--border);
border-radius: 20px;
color: var(--text-secondary);
font-size: 14px;
cursor: pointer;
transition: all 0.2s ease;
}
[data-theme="sentient"] .status-filter:hover,
[data-theme="sentient"] .filter-tab:hover {
background: var(--surface-hover);
color: var(--text);
}
[data-theme="sentient"] .status-filter.active,
[data-theme="sentient"] .filter-tab.active {
background: var(--accent);
color: #000000;
border-color: var(--accent);
font-weight: 600;
}
[data-theme="sentient"] .status-filter .count,
[data-theme="sentient"] .filter-tab .count {
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 20px;
height: 20px;
padding: 0 6px;
background: var(--surface-hover);
border-radius: 10px;
font-size: 12px;
font-weight: 600;
}
[data-theme="sentient"] .status-filter.active .count,
[data-theme="sentient"] .filter-tab.active .count {
background: rgba(0, 0, 0, 0.2);
color: #000000;
}
/* ============================================ */
/* PROGRESS BARS */
/* ============================================ */
[data-theme="sentient"] .progress-bar {
height: 8px;
background: var(--surface-hover);
border-radius: 4px;
overflow: hidden;
}
[data-theme="sentient"] .progress-bar-fill {
height: 100%;
background: var(--accent);
border-radius: 4px;
transition: width 0.3s ease;
}
[data-theme="sentient"] .progress-percentage {
color: var(--accent);
font-weight: 600;
font-size: 14px;
}
/* ============================================ */
/* BADGES & TAGS */
/* ============================================ */
[data-theme="sentient"] .badge {
padding: 4px 10px;
border-radius: 12px;
font-size: 12px;
font-weight: 500;
}
[data-theme="sentient"] .badge-primary,
[data-theme="sentient"] .badge-active {
background: var(--accent-light);
color: var(--accent);
}
[data-theme="sentient"] .badge-success,
[data-theme="sentient"] .badge-complete {
background: var(--success-bg);
color: var(--success);
}
[data-theme="sentient"] .badge-warning,
[data-theme="sentient"] .badge-pending {
background: var(--warning-bg);
color: var(--warning);
}
[data-theme="sentient"] .badge-danger,
[data-theme="sentient"] .badge-blocked {
background: var(--error-bg);
color: var(--error);
}
[data-theme="sentient"] .badge-secondary,
[data-theme="sentient"] .badge-paused {
background: var(--surface-hover);
color: var(--text-secondary);
}
/* Tag pills */
[data-theme="sentient"] .tag {
display: inline-flex;
align-items: center;
padding: 4px 12px;
background: var(--surface);
border: 1px solid var(--border);
border-radius: 16px;
color: var(--text-secondary);
font-size: 12px;
}
/* ============================================ */
/* INTENT/TASK CARDS */
/* ============================================ */
[data-theme="sentient"] .intent-card {
padding: 20px;
margin-bottom: 16px;
}
[data-theme="sentient"] .intent-card-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 16px;
}
[data-theme="sentient"] .intent-title {
font-size: 16px;
font-weight: 600;
color: var(--text);
}
[data-theme="sentient"] .intent-status {
display: flex;
align-items: center;
gap: 8px;
color: var(--text-secondary);
font-size: 14px;
}
[data-theme="sentient"] .intent-status.active {
color: var(--accent);
}
[data-theme="sentient"] .intent-meta {
display: flex;
flex-wrap: wrap;
gap: 12px;
margin-top: 16px;
}
[data-theme="sentient"] .intent-meta-item {
display: inline-flex;
align-items: center;
padding: 6px 12px;
background: var(--surface-hover);
border-radius: 8px;
font-size: 13px;
color: var(--text-secondary);
}
[data-theme="sentient"] .intent-health {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 6px 12px;
background: var(--accent-light);
border-radius: 8px;
font-size: 13px;
font-weight: 600;
}
[data-theme="sentient"] .intent-health-good {
color: var(--accent);
}
[data-theme="sentient"] .intent-health-warning {
background: var(--warning-bg);
color: var(--warning);
}
[data-theme="sentient"] .intent-health-bad {
background: var(--error-bg);
color: var(--error);
}
/* ============================================ */
/* DECISION REQUIRED PANEL */
/* ============================================ */
[data-theme="sentient"] .decision-panel {
background: var(--surface);
border: 1px solid var(--accent);
border-radius: 12px;
padding: 20px;
box-shadow: var(--shadow-glow);
}
[data-theme="sentient"] .decision-header {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 12px;
color: var(--accent);
font-weight: 700;
font-size: 14px;
text-transform: uppercase;
letter-spacing: 0.5px;
}
[data-theme="sentient"] .decision-title {
color: var(--text);
font-size: 16px;
font-weight: 500;
margin-bottom: 16px;
}
[data-theme="sentient"] .decision-context {
background: var(--surface-hover);
border-radius: 8px;
padding: 16px;
color: var(--text-secondary);
font-size: 14px;
line-height: 1.6;
margin-bottom: 16px;
}
[data-theme="sentient"] .decision-actions {
display: flex;
gap: 12px;
}
[data-theme="sentient"] .decision-btn-primary {
background: var(--accent);
color: #000000;
border: none;
padding: 10px 20px;
border-radius: 8px;
font-weight: 600;
cursor: pointer;
transition: all 0.2s ease;
}
[data-theme="sentient"] .decision-btn-primary:hover {
background: var(--accent-hover);
box-shadow: var(--shadow-accent);
}
[data-theme="sentient"] .decision-btn-secondary {
background: transparent;
color: var(--text);
border: 1px solid var(--border);
padding: 10px 20px;
border-radius: 8px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s ease;
}
[data-theme="sentient"] .decision-btn-secondary:hover {
background: var(--surface-hover);
border-color: var(--text-secondary);
}
/* ============================================ */
/* PROGRESS LOG */
/* ============================================ */
[data-theme="sentient"] .progress-log {
background: var(--surface);
border: 1px solid var(--border);
border-radius: 12px;
padding: 20px;
}
[data-theme="sentient"] .progress-log-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
color: var(--text);
font-weight: 600;
font-size: 14px;
text-transform: uppercase;
letter-spacing: 0.5px;
}
[data-theme="sentient"] .progress-log-item {
display: flex;
align-items: flex-start;
gap: 12px;
padding: 12px 0;
border-bottom: 1px solid var(--border-light);
}
[data-theme="sentient"] .progress-log-item:last-child {
border-bottom: none;
}
[data-theme="sentient"] .progress-log-icon {
width: 24px;
height: 24px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
flex-shrink: 0;
}
[data-theme="sentient"] .progress-log-icon.complete {
background: var(--success-bg);
color: var(--success);
}
[data-theme="sentient"] .progress-log-icon.pending {
background: var(--warning-bg);
color: var(--warning);
}
[data-theme="sentient"] .progress-log-icon.active {
background: var(--accent-light);
color: var(--accent);
}
[data-theme="sentient"] .progress-log-content {
flex: 1;
}
[data-theme="sentient"] .progress-log-title {
color: var(--text);
font-weight: 500;
margin-bottom: 4px;
}
[data-theme="sentient"] .progress-log-detail {
color: var(--text-tertiary);
font-size: 13px;
}
[data-theme="sentient"] .progress-log-step {
display: inline-flex;
align-items: center;
padding: 4px 10px;
background: var(--accent);
color: #000000;
border-radius: 10px;
font-size: 12px;
font-weight: 600;
}
[data-theme="sentient"] .progress-log-step.complete {
background: var(--success);
}
[data-theme="sentient"] .progress-log-time {
color: var(--text-tertiary);
font-size: 12px;
flex-shrink: 0;
}
/* ============================================ */
/* SIDEBAR / NAVIGATION */
/* ============================================ */
[data-theme="sentient"] .sidebar {
background: var(--sidebar-bg);
border-right: 1px solid var(--sidebar-border);
}
[data-theme="sentient"] .sidebar-item {
color: var(--text-secondary);
padding: 10px 16px;
border-radius: 8px;
margin: 4px 8px;
transition: all 0.2s ease;
}
[data-theme="sentient"] .sidebar-item:hover {
background: var(--surface-hover);
color: var(--text);
}
[data-theme="sentient"] .sidebar-item.active {
background: var(--accent-light);
color: var(--accent);
}
/* ============================================ */
/* DROPDOWN MENUS */
/* ============================================ */
[data-theme="sentient"] .apps-dropdown,
[data-theme="sentient"] .dropdown-menu {
background: var(--surface);
border: 1px solid var(--border);
border-radius: 12px;
box-shadow: var(--shadow-lg);
}
[data-theme="sentient"] .apps-dropdown-title {
color: var(--text-secondary);
font-size: 12px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.5px;
padding: 12px 16px;
border-bottom: 1px solid var(--border);
}
[data-theme="sentient"] .app-item {
color: var(--text-secondary);
padding: 12px;
border-radius: 8px;
transition: all 0.2s ease;
}
[data-theme="sentient"] .app-item:hover {
background: var(--surface-hover);
color: var(--text);
}
[data-theme="sentient"] .app-item.active {
background: var(--accent-light);
color: var(--accent);
}
/* ============================================ */
/* FORMS & INPUTS */
/* ============================================ */
[data-theme="sentient"] input,
[data-theme="sentient"] textarea,
[data-theme="sentient"] select {
background: var(--input-bg);
border: 1px solid var(--input-border);
color: var(--text);
border-radius: 8px;
padding: 10px 14px;
transition: all 0.2s ease;
}
[data-theme="sentient"] input:focus,
[data-theme="sentient"] textarea:focus,
[data-theme="sentient"] select:focus {
border-color: var(--input-focus-border);
outline: none;
box-shadow: 0 0 0 3px var(--accent-light);
}
[data-theme="sentient"] input::placeholder,
[data-theme="sentient"] textarea::placeholder {
color: var(--text-tertiary);
}
/* ============================================ */
/* MODALS */
/* ============================================ */
[data-theme="sentient"] .modal-content {
background: var(--surface);
border: 1px solid var(--border);
border-radius: 16px;
box-shadow: var(--shadow-lg);
}
[data-theme="sentient"] .modal-header {
border-bottom: 1px solid var(--border);
padding: 20px 24px;
}
[data-theme="sentient"] .modal-header h3 {
color: var(--text);
}
[data-theme="sentient"] .modal-body {
padding: 24px;
}
[data-theme="sentient"] .modal-footer {
border-top: 1px solid var(--border);
padding: 16px 24px;
}
/* ============================================ */
/* TABLES */
/* ============================================ */
[data-theme="sentient"] table {
width: 100%;
border-collapse: collapse;
}
[data-theme="sentient"] th {
background: var(--surface-hover);
color: var(--text-secondary);
font-weight: 600;
font-size: 12px;
text-transform: uppercase;
letter-spacing: 0.5px;
padding: 12px 16px;
text-align: left;
border-bottom: 1px solid var(--border);
}
[data-theme="sentient"] td {
padding: 14px 16px;
border-bottom: 1px solid var(--border-light);
color: var(--text);
}
[data-theme="sentient"] tr:hover td {
background: var(--surface-hover);
}
/* ============================================ */
/* SCROLLBAR */
/* ============================================ */
[data-theme="sentient"] ::-webkit-scrollbar {
width: 8px;
height: 8px;
}
[data-theme="sentient"] ::-webkit-scrollbar-track {
background: var(--bg);
}
[data-theme="sentient"] ::-webkit-scrollbar-thumb {
background: var(--surface-hover);
border-radius: 4px;
}
[data-theme="sentient"] ::-webkit-scrollbar-thumb:hover {
background: var(--border);
}
/* ============================================ */
/* LOADING STATES */
/* ============================================ */
[data-theme="sentient"] .loading-overlay {
background: rgba(10, 10, 10, 0.9);
}
[data-theme="sentient"] .loading-spinner {
border-color: var(--surface-hover);
border-top-color: var(--accent);
}
[data-theme="sentient"] .skeleton {
background: linear-gradient(
90deg,
var(--surface) 25%,
var(--surface-hover) 50%,
var(--surface) 75%
);
background-size: 200% 100%;
animation: skeleton-loading 1.5s infinite;
}
@keyframes skeleton-loading {
0% {
background-position: 200% 0;
}
100% {
background-position: -200% 0;
}
}
/* ============================================ */
/* CHAT SPECIFIC */
/* ============================================ */
[data-theme="sentient"] .message-user {
background: var(--accent);
color: #000000;
}
[data-theme="sentient"] .message-bot {
background: var(--surface);
color: var(--text);
border: 1px solid var(--border);
}
[data-theme="sentient"] .chat-input-container {
background: var(--surface);
border: 1px solid var(--border);
border-radius: 12px;
}
[data-theme="sentient"] .chat-input-container:focus-within {
border-color: var(--accent);
}
/* ============================================ */
/* ANIMATIONS */
/* ============================================ */
[data-theme="sentient"] .pulse-accent {
animation: pulse-accent 2s infinite;
}
@keyframes pulse-accent {
0%, 100% {
box-shadow: 0 0 0 0 var(--accent-glow);
}
50% {
box-shadow: 0 0 0 10px transparent;
}
}
/* ============================================ */
/* STATS & METRICS */
/* ============================================ */
[data-theme="sentient"] .stat-card {
background: var(--surface);
border: 1px solid var(--border);
border-radius: 12px;
padding: 20px;
}
[data-theme="sentient"] .stat-value {
color: var(--text);
font-size: 28px;
font-weight: 700;
}
[data-theme="sentient"] .stat-label {
color: var(--text-secondary);
font-size: 14px;
margin-top: 4px;
}
[data-theme="sentient"] .stat-highlight {
color: var(--accent);
}
/* Time saved indicator */
[data-theme="sentient"] .time-saved {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 8px 16px;
background: var(--accent-light);
border-radius: 20px;
color: var(--accent);
font-weight: 600;
font-size: 14px;
}
/* ============================================ */
/* DETAIL VIEW */
/* ============================================ */
[data-theme="sentient"] .detail-panel {
background: var(--surface);
border: 1px solid var(--border);
border-radius: 12px;
}
[data-theme="sentient"] .detail-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 20px;
border-bottom: 1px solid var(--border);
}
[data-theme="sentient"] .detail-title {
font-size: 18px;
font-weight: 600;
color: var(--text);
}
[data-theme="sentient"] .detail-body {
padding: 20px;
}
[data-theme="sentient"] .detail-section {
margin-bottom: 24px;
}
[data-theme="sentient"] .detail-section-title {
color: var(--text-secondary);
font-size: 12px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.5px;
margin-bottom: 12px;
}
/* ============================================ */
/* PAUSE/ACTION BUTTONS */
/* ============================================ */
[data-theme="sentient"] .action-btn {
display: inline-flex;
align-items: center;
justify-content: center;
width: 36px;
height: 36px;
border-radius: 8px;
border: 1px solid var(--border);
background: transparent;
color: var(--text-secondary);
cursor: pointer;
transition: all 0.2s ease;
}
[data-theme="sentient"] .action-btn:hover {
background: var(--surface-hover);
color: var(--text);
border-color: var(--text-secondary);
}
[data-theme="sentient"] .action-btn.pause {
border-color: var(--warning);
color: var(--warning);
}
[data-theme="sentient"] .action-btn.pause:hover {

View file

@ -2,17 +2,22 @@
.drive-container { .drive-container {
display: flex; display: flex;
height: calc(100vh - 60px); height: 100%;
background: var(--bg); min-height: 0;
background: var(--bg, var(--bg-primary, #0a0a0f));
color: var(--text, var(--text-primary, #ffffff));
} }
/* Drive Sidebar */ /* Drive Sidebar */
.drive-sidebar { .drive-sidebar {
width: 240px; width: 240px;
background: var(--surface); min-width: 240px;
border-right: 1px solid var(--border); background: var(--surface, var(--bg-secondary, #12121a));
border-right: 1px solid var(--border, #2a2a3a);
display: flex; display: flex;
flex-direction: column; flex-direction: column;
flex-shrink: 0;
overflow: hidden;
} }
.drive-sidebar-header { .drive-sidebar-header {
@ -76,6 +81,87 @@
display: flex; display: flex;
flex-direction: column; flex-direction: column;
overflow: hidden; overflow: hidden;
min-width: 0;
}
/* Drive Header */
.drive-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 16px 24px;
border-bottom: 1px solid var(--border, #2a2a3a);
flex-shrink: 0;
}
/* Drive Content - File Grid */
.drive-content {
flex: 1;
overflow-y: auto;
padding: 24px;
}
.file-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
gap: 16px;
}
.file-list {
display: flex;
flex-direction: column;
gap: 2px;
}
.file-list-header {
display: grid;
grid-template-columns: 1fr 150px 100px;
gap: 16px;
padding: 8px 16px;
font-size: 12px;
font-weight: 600;
color: var(--text-secondary, #6b6b80);
border-bottom: 1px solid var(--border, #2a2a3a);
}
.file-list-item,
.drive-file-item {
display: grid;
grid-template-columns: 1fr 150px 100px;
gap: 16px;
padding: 12px 16px;
align-items: center;
border-radius: 8px;
cursor: pointer;
transition: background 0.15s;
}
.file-list-item:hover,
.drive-file-item:hover {
background: var(--surface-hover, rgba(255, 255, 255, 0.05));
}
.file-list-item.selected,
.drive-file-item.selected {
background: var(--accent-light, rgba(59, 130, 246, 0.1));
}
.file-name {
display: flex;
align-items: center;
gap: 12px;
font-weight: 500;
color: var(--text, var(--text-primary, #ffffff));
}
.file-icon {
font-size: 20px;
}
.file-date,
.file-size {
font-size: 13px;
color: var(--text-secondary, #6b6b80);
} }
.drive-toolbar { .drive-toolbar {
@ -164,11 +250,26 @@
border-radius: 8px; border-radius: 8px;
} }
.file-icon.folder { background: rgba(249, 115, 22, 0.1); color: #f97316; } .file-icon.folder {
.file-icon.document { background: rgba(59, 130, 246, 0.1); color: #3b82f6; } background: rgba(249, 115, 22, 0.1);
.file-icon.image { background: rgba(168, 85, 247, 0.1); color: #a855f7; } color: #f97316;
.file-icon.video { background: rgba(239, 68, 68, 0.1); color: #ef4444; } }
.file-icon.audio { background: rgba(34, 197, 94, 0.1); color: #22c55e; } .file-icon.document {
background: rgba(59, 130, 246, 0.1);
color: #3b82f6;
}
.file-icon.image {
background: rgba(168, 85, 247, 0.1);
color: #a855f7;
}
.file-icon.video {
background: rgba(239, 68, 68, 0.1);
color: #ef4444;
}
.file-icon.audio {
background: rgba(34, 197, 94, 0.1);
color: #22c55e;
}
.file-name { .file-name {
font-size: 13px; font-size: 13px;

View file

@ -170,24 +170,6 @@
</div> </div>
</div> </div>
<div class="drive-toolbar-right"> <div class="drive-toolbar-right">
<div class="drive-search">
<svg
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<circle cx="11" cy="11" r="8"></circle>
<line x1="21" y1="21" x2="16.65" y2="16.65"></line>
</svg>
<input
type="text"
placeholder="Search files..."
id="drive-search-input"
/>
</div>
<button <button
class="btn-icon view-toggle" class="btn-icon view-toggle"
data-view="grid" data-view="grid"

View file

@ -1,11 +1,35 @@
/* Drive Module JavaScript */ /* Drive Module JavaScript */
(function() { (function () {
'use strict'; "use strict";
let selectedFiles = []; let selectedFiles = [];
let currentPath = '/'; let currentPath = "/";
let viewMode = 'grid'; let viewMode = "grid";
// Global function for onclick handlers in HTML
window.selectFile = function (element) {
const fileId = element.dataset.id;
const isSelected = element.classList.contains("selected");
// If not holding Ctrl/Cmd, deselect all others first
if (!event.ctrlKey && !event.metaKey) {
document
.querySelectorAll(
".drive-file-item.selected, .file-item.selected, .file-card.selected, .file-list-item.selected",
)
.forEach((f) => f.classList.remove("selected"));
}
// Toggle selection on clicked element
if (isSelected && (event.ctrlKey || event.metaKey)) {
element.classList.remove("selected");
} else {
element.classList.add("selected");
}
updateSelectedFiles();
};
function init() { function init() {
bindNavigation(); bindNavigation();
@ -16,29 +40,32 @@
} }
function bindNavigation() { function bindNavigation() {
document.querySelectorAll('.drive-nav-item').forEach(item => { document.querySelectorAll(".drive-nav-item").forEach((item) => {
item.addEventListener('click', function() { item.addEventListener("click", function () {
document.querySelectorAll('.drive-nav-item').forEach(i => i.classList.remove('active')); document
this.classList.add('active'); .querySelectorAll(".drive-nav-item")
.forEach((i) => i.classList.remove("active"));
this.classList.add("active");
}); });
}); });
} }
function bindFileSelection() { function bindFileSelection() {
document.querySelectorAll('.file-card, .file-list-item').forEach(file => { document.querySelectorAll(".file-card, .file-list-item").forEach((file) => {
file.addEventListener('click', function(e) { file.addEventListener("click", function (e) {
if (e.ctrlKey || e.metaKey) { if (e.ctrlKey || e.metaKey) {
this.classList.toggle('selected'); this.classList.toggle("selected");
} else { } else {
document.querySelectorAll('.file-card.selected, .file-list-item.selected') document
.forEach(f => f.classList.remove('selected')); .querySelectorAll(".file-card.selected, .file-list-item.selected")
this.classList.add('selected'); .forEach((f) => f.classList.remove("selected"));
this.classList.add("selected");
} }
updateSelectedFiles(); updateSelectedFiles();
}); });
file.addEventListener('dblclick', function() { file.addEventListener("dblclick", function () {
const isFolder = this.dataset.type === 'folder'; const isFolder = this.dataset.type === "folder";
if (isFolder) { if (isFolder) {
navigateToFolder(this.dataset.path); navigateToFolder(this.dataset.path);
} else { } else {
@ -49,52 +76,56 @@
} }
function bindDragAndDrop() { function bindDragAndDrop() {
const uploadZone = document.querySelector('.upload-zone'); const uploadZone = document.querySelector(".upload-zone");
if (!uploadZone) return; if (!uploadZone) return;
['dragenter', 'dragover', 'dragleave', 'drop'].forEach(event => { ["dragenter", "dragover", "dragleave", "drop"].forEach((event) => {
uploadZone.addEventListener(event, (e) => { uploadZone.addEventListener(event, (e) => {
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
}); });
}); });
['dragenter', 'dragover'].forEach(event => { ["dragenter", "dragover"].forEach((event) => {
uploadZone.addEventListener(event, () => uploadZone.classList.add('dragover')); uploadZone.addEventListener(event, () =>
uploadZone.classList.add("dragover"),
);
}); });
['dragleave', 'drop'].forEach(event => { ["dragleave", "drop"].forEach((event) => {
uploadZone.addEventListener(event, () => uploadZone.classList.remove('dragover')); uploadZone.addEventListener(event, () =>
uploadZone.classList.remove("dragover"),
);
}); });
uploadZone.addEventListener('drop', (e) => { uploadZone.addEventListener("drop", (e) => {
const files = e.dataTransfer.files; const files = e.dataTransfer.files;
handleFileUpload(files); handleFileUpload(files);
}); });
} }
function bindContextMenu() { function bindContextMenu() {
document.addEventListener('contextmenu', (e) => { document.addEventListener("contextmenu", (e) => {
const fileCard = e.target.closest('.file-card, .file-list-item'); const fileCard = e.target.closest(".file-card, .file-list-item");
if (fileCard) { if (fileCard) {
e.preventDefault(); e.preventDefault();
showContextMenu(e.clientX, e.clientY, fileCard); showContextMenu(e.clientX, e.clientY, fileCard);
} }
}); });
document.addEventListener('click', hideContextMenu); document.addEventListener("click", hideContextMenu);
} }
function bindKeyboardShortcuts() { function bindKeyboardShortcuts() {
document.addEventListener('keydown', (e) => { document.addEventListener("keydown", (e) => {
if (e.key === 'Delete' && selectedFiles.length > 0) { if (e.key === "Delete" && selectedFiles.length > 0) {
deleteSelectedFiles(); deleteSelectedFiles();
} }
if (e.key === 'a' && (e.ctrlKey || e.metaKey)) { if (e.key === "a" && (e.ctrlKey || e.metaKey)) {
e.preventDefault(); e.preventDefault();
selectAllFiles(); selectAllFiles();
} }
if (e.key === 'Escape') { if (e.key === "Escape") {
deselectAllFiles(); deselectAllFiles();
hideContextMenu(); hideContextMenu();
} }
@ -102,8 +133,11 @@
} }
function updateSelectedFiles() { function updateSelectedFiles() {
selectedFiles = Array.from(document.querySelectorAll('.file-card.selected, .file-list-item.selected')) selectedFiles = Array.from(
.map(f => f.dataset.id); document.querySelectorAll(
".file-card.selected, .file-list-item.selected",
),
).map((f) => f.dataset.id);
} }
function navigateToFolder(path) { function navigateToFolder(path) {
@ -112,50 +146,53 @@
} }
function openFile(fileId) { function openFile(fileId) {
console.log('Opening file:', fileId); console.log("Opening file:", fileId);
} }
function handleFileUpload(files) { function handleFileUpload(files) {
console.log('Uploading files:', files); console.log("Uploading files:", files);
} }
function showContextMenu(x, y, fileCard) { function showContextMenu(x, y, fileCard) {
hideContextMenu(); hideContextMenu();
const menu = document.getElementById('context-menu'); const menu = document.getElementById("context-menu");
if (menu) { if (menu) {
menu.style.left = x + 'px'; menu.style.left = x + "px";
menu.style.top = y + 'px'; menu.style.top = y + "px";
menu.classList.remove('hidden'); menu.classList.remove("hidden");
menu.dataset.fileId = fileCard.dataset.id; menu.dataset.fileId = fileCard.dataset.id;
} }
} }
function hideContextMenu() { function hideContextMenu() {
const menu = document.getElementById('context-menu'); const menu = document.getElementById("context-menu");
if (menu) menu.classList.add('hidden'); if (menu) menu.classList.add("hidden");
} }
function selectAllFiles() { function selectAllFiles() {
document.querySelectorAll('.file-card, .file-list-item').forEach(f => f.classList.add('selected')); document
.querySelectorAll(".file-card, .file-list-item")
.forEach((f) => f.classList.add("selected"));
updateSelectedFiles(); updateSelectedFiles();
} }
function deselectAllFiles() { function deselectAllFiles() {
document.querySelectorAll('.file-card.selected, .file-list-item.selected') document
.forEach(f => f.classList.remove('selected')); .querySelectorAll(".file-card.selected, .file-list-item.selected")
.forEach((f) => f.classList.remove("selected"));
selectedFiles = []; selectedFiles = [];
} }
function deleteSelectedFiles() { function deleteSelectedFiles() {
if (confirm(`Delete ${selectedFiles.length} file(s)?`)) { if (confirm(`Delete ${selectedFiles.length} file(s)?`)) {
console.log('Deleting:', selectedFiles); console.log("Deleting:", selectedFiles);
} }
} }
window.DriveModule = { init }; window.DriveModule = { init };
if (document.readyState === 'loading') { if (document.readyState === "loading") {
document.addEventListener('DOMContentLoaded', init); document.addEventListener("DOMContentLoaded", init);
} else { } else {
init(); init();
} }

View file

@ -148,21 +148,7 @@
</div> </div>
</div> </div>
<div class="toolbar-center">
<div class="search-box">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="11" cy="11" r="8"></circle>
<line x1="21" y1="21" x2="16.65" y2="16.65"></line>
</svg>
<input type="text"
placeholder="Search in Drive..."
name="q"
hx-get="/api/drive/search"
hx-trigger="keyup changed delay:300ms"
hx-target="#file-grid"
hx-swap="innerHTML">
</div>
</div>
<div class="toolbar-actions"> <div class="toolbar-actions">
<div class="view-toggle"> <div class="view-toggle">

View file

@ -8,13 +8,14 @@
name="description" name="description"
content="General Bots - AI-powered workspace" content="General Bots - AI-powered workspace"
/> />
<meta name="theme-color" content="#3b82f6" /> <meta name="theme-color" content="#d4f505" />
<!-- Styles --> <!-- Styles -->
<link rel="stylesheet" href="css/app.css" /> <link rel="stylesheet" href="css/app.css" />
<link rel="stylesheet" href="css/apps-extended.css" /> <link rel="stylesheet" href="css/apps-extended.css" />
<link rel="stylesheet" href="css/components.css" /> <link rel="stylesheet" href="css/components.css" />
<link rel="stylesheet" href="css/base.css" /> <link rel="stylesheet" href="css/base.css" />
<link rel="stylesheet" href="css/theme-sentient.css" />
<!-- App-specific CSS --> <!-- App-specific CSS -->
<link rel="stylesheet" href="chat/chat.css" /> <link rel="stylesheet" href="chat/chat.css" />
@ -41,7 +42,7 @@
</script> </script>
</head> </head>
<body> <body data-theme="sentient">
<!-- Loading overlay --> <!-- Loading overlay -->
<div class="loading-overlay" id="loadingOverlay"> <div class="loading-overlay" id="loadingOverlay">
<div class="loading-spinner"></div> <div class="loading-spinner"></div>
@ -231,7 +232,13 @@
</svg> </svg>
<span class="notification-badge">3</span> <span class="notification-badge">3</span>
</button> </button>
<button class="icon-button" title="Settings"> <button
class="icon-button"
id="settingsBtn"
title="Settings"
aria-expanded="false"
aria-haspopup="true"
>
<svg <svg
width="20" width="20"
height="20" height="20"
@ -247,6 +254,108 @@
</svg> </svg>
</button> </button>
<!-- Settings/Theme Panel -->
<div
class="settings-panel"
id="settingsPanel"
role="menu"
aria-label="Settings"
>
<div class="settings-panel-title">Theme</div>
<div class="theme-grid">
<button
class="theme-option theme-sentient"
data-theme="sentient"
title="Sentient"
>
<div class="theme-option-inner">
<div class="theme-option-header">
<div class="theme-option-dot"></div>
</div>
<div class="theme-option-body">
<div class="theme-option-line"></div>
</div>
</div>
<span class="theme-option-name">Sentient</span>
</button>
<button
class="theme-option theme-dark"
data-theme="dark"
title="Dark"
>
<div class="theme-option-inner">
<div class="theme-option-header">
<div class="theme-option-dot"></div>
</div>
<div class="theme-option-body">
<div class="theme-option-line"></div>
</div>
</div>
<span class="theme-option-name">Dark</span>
</button>
<button
class="theme-option theme-light"
data-theme="light"
title="Light"
>
<div class="theme-option-inner">
<div class="theme-option-header">
<div class="theme-option-dot"></div>
</div>
<div class="theme-option-body">
<div class="theme-option-line"></div>
</div>
</div>
<span class="theme-option-name">Light</span>
</button>
<button
class="theme-option theme-orange"
data-theme="orange"
title="Orange"
>
<div class="theme-option-inner">
<div class="theme-option-header">
<div class="theme-option-dot"></div>
</div>
<div class="theme-option-body">
<div class="theme-option-line"></div>
</div>
</div>
<span class="theme-option-name">Orange</span>
</button>
<button
class="theme-option theme-purple"
data-theme="purple"
title="Purple"
>
<div class="theme-option-inner">
<div class="theme-option-header">
<div class="theme-option-dot"></div>
</div>
<div class="theme-option-body">
<div class="theme-option-line"></div>
</div>
</div>
<span class="theme-option-name">Purple</span>
</button>
<button
class="theme-option theme-green"
data-theme="green"
title="Green"
>
<div class="theme-option-inner">
<div class="theme-option-header">
<div class="theme-option-dot"></div>
</div>
<div class="theme-option-body">
<div class="theme-option-line"></div>
</div>
</div>
<span class="theme-option-name">Green</span>
</button>
</div>
</div>
<!-- Apps dropdown menu --> <!-- Apps dropdown menu -->
<nav <nav
class="apps-dropdown" class="apps-dropdown"
@ -804,12 +913,17 @@
// Simple apps menu handling // Simple apps menu handling
const appsBtn = document.getElementById("appsButton"); const appsBtn = document.getElementById("appsButton");
const appsDropdown = document.getElementById("appsDropdown"); const appsDropdown = document.getElementById("appsDropdown");
const settingsBtn = document.getElementById("settingsBtn");
const settingsPanel = document.getElementById("settingsPanel");
if (appsBtn && appsDropdown) { if (appsBtn && appsDropdown) {
appsBtn.addEventListener("click", (e) => { appsBtn.addEventListener("click", (e) => {
e.stopPropagation(); e.stopPropagation();
const isOpen = appsDropdown.classList.toggle("show"); const isOpen = appsDropdown.classList.toggle("show");
appsBtn.setAttribute("aria-expanded", isOpen); appsBtn.setAttribute("aria-expanded", isOpen);
// Close settings panel
if (settingsPanel)
settingsPanel.classList.remove("show");
}); });
document.addEventListener("click", (e) => { document.addEventListener("click", (e) => {
@ -823,6 +937,69 @@
}); });
} }
// Settings panel handling
if (settingsBtn && settingsPanel) {
settingsBtn.addEventListener("click", (e) => {
e.stopPropagation();
const isOpen = settingsPanel.classList.toggle("show");
settingsBtn.setAttribute("aria-expanded", isOpen);
// Close apps dropdown
if (appsDropdown) appsDropdown.classList.remove("show");
});
document.addEventListener("click", (e) => {
if (
!settingsPanel.contains(e.target) &&
!settingsBtn.contains(e.target)
) {
settingsPanel.classList.remove("show");
settingsBtn.setAttribute("aria-expanded", "false");
}
});
}
// Theme selection handling
const themeOptions = document.querySelectorAll(".theme-option");
const savedTheme =
localStorage.getItem("gb-theme") || "sentient";
// Apply saved theme
document.body.setAttribute("data-theme", savedTheme);
document
.querySelector(`.theme-option[data-theme="${savedTheme}"]`)
?.classList.add("active");
themeOptions.forEach((option) => {
option.addEventListener("click", () => {
const theme = option.getAttribute("data-theme");
document.body.setAttribute("data-theme", theme);
localStorage.setItem("gb-theme", theme);
themeOptions.forEach((o) =>
o.classList.remove("active"),
);
option.classList.add("active");
// Update theme-color meta tag
const themeColors = {
dark: "#3b82f6",
light: "#3b82f6",
purple: "#a855f7",
green: "#22c55e",
orange: "#f97316",
sentient: "#d4f505",
};
const metaTheme = document.querySelector(
'meta[name="theme-color"]',
);
if (metaTheme) {
metaTheme.setAttribute(
"content",
themeColors[theme] || "#d4f505",
);
}
});
});
// Handle app item clicks - update active state // Handle app item clicks - update active state
document.querySelectorAll(".app-item").forEach((item) => { document.querySelectorAll(".app-item").forEach((item) => {
item.addEventListener("click", function () { item.addEventListener("click", function () {

View file

@ -32,11 +32,19 @@ if (settingsBtn) {
// Close dropdowns when clicking outside // Close dropdowns when clicking outside
document.addEventListener("click", (e) => { document.addEventListener("click", (e) => {
if (appsDropdown && !appsDropdown.contains(e.target) && !appsBtn.contains(e.target)) { if (
appsDropdown &&
!appsDropdown.contains(e.target) &&
!appsBtn.contains(e.target)
) {
appsDropdown.classList.remove("show"); appsDropdown.classList.remove("show");
appsBtn.setAttribute("aria-expanded", "false"); appsBtn.setAttribute("aria-expanded", "false");
} }
if (settingsPanel && !settingsPanel.contains(e.target) && !settingsBtn.contains(e.target)) { if (
settingsPanel &&
!settingsPanel.contains(e.target) &&
!settingsBtn.contains(e.target)
) {
settingsPanel.classList.remove("show"); settingsPanel.classList.remove("show");
settingsBtn.setAttribute("aria-expanded", "false"); settingsBtn.setAttribute("aria-expanded", "false");
} }
@ -99,10 +107,31 @@ document.body.addEventListener("htmx:afterSwap", (e) => {
}); });
// Theme handling // Theme handling
// Available themes: dark, light, blue, purple, green, orange, sentient
const themeOptions = document.querySelectorAll(".theme-option"); const themeOptions = document.querySelectorAll(".theme-option");
const savedTheme = localStorage.getItem("gb-theme") || "dark"; const savedTheme = localStorage.getItem("gb-theme") || "sentient";
document.body.setAttribute("data-theme", savedTheme); document.body.setAttribute("data-theme", savedTheme);
document.querySelector(`.theme-option[data-theme="${savedTheme}"]`)?.classList.add("active"); document
.querySelector(`.theme-option[data-theme="${savedTheme}"]`)
?.classList.add("active");
// Update theme-color meta tag based on theme
function updateThemeColor(theme) {
const themeColors = {
dark: "#3b82f6",
light: "#3b82f6",
blue: "#0ea5e9",
purple: "#a855f7",
green: "#22c55e",
orange: "#f97316",
sentient: "#d4f505",
};
const metaTheme = document.querySelector('meta[name="theme-color"]');
if (metaTheme) {
metaTheme.setAttribute("content", themeColors[theme] || "#d4f505");
}
}
updateThemeColor(savedTheme);
themeOptions.forEach((option) => { themeOptions.forEach((option) => {
option.addEventListener("click", () => { option.addEventListener("click", () => {
@ -111,9 +140,20 @@ themeOptions.forEach((option) => {
localStorage.setItem("gb-theme", theme); localStorage.setItem("gb-theme", theme);
themeOptions.forEach((o) => o.classList.remove("active")); themeOptions.forEach((o) => o.classList.remove("active"));
option.classList.add("active"); option.classList.add("active");
updateThemeColor(theme);
}); });
}); });
// Global theme setter function (can be called from settings or elsewhere)
window.setTheme = function (theme) {
document.body.setAttribute("data-theme", theme);
localStorage.setItem("gb-theme", theme);
themeOptions.forEach((o) => {
o.classList.toggle("active", o.getAttribute("data-theme") === theme);
});
updateThemeColor(theme);
};
// Quick Settings Toggle // Quick Settings Toggle
function toggleQuickSetting(el) { function toggleQuickSetting(el) {
el.classList.toggle("active"); el.classList.toggle("active");
@ -135,7 +175,7 @@ function showKeyboardShortcuts() {
window.showNotification( window.showNotification(
"Alt+1-9,0 for apps, Alt+A Admin, Alt+M Monitoring, Alt+S Settings, Alt+, quick settings", "Alt+1-9,0 for apps, Alt+A Admin, Alt+M Monitoring, Alt+S Settings, Alt+, quick settings",
"info", "info",
8000 8000,
); );
} }
@ -184,7 +224,9 @@ document.addEventListener("keydown", function (e) {
if (!appsGrid || !appsGrid.closest(".show")) return; if (!appsGrid || !appsGrid.closest(".show")) return;
const items = Array.from(appsGrid.querySelectorAll(".app-item")); const items = Array.from(appsGrid.querySelectorAll(".app-item"));
const currentIndex = items.findIndex((item) => item === document.activeElement); const currentIndex = items.findIndex(
(item) => item === document.activeElement,
);
if (currentIndex === -1) return; if (currentIndex === -1) return;
@ -317,14 +359,17 @@ document.body.addEventListener("htmx:responseError", function (e) {
let currentRetries = htmxRetryConfig.retryCount.get(retryKey) || 0; let currentRetries = htmxRetryConfig.retryCount.get(retryKey) || 0;
// Auto-retry for network errors (status 0) or server errors (5xx) // Auto-retry for network errors (status 0) or server errors (5xx)
if ((xhr.status === 0 || xhr.status >= 500) && currentRetries < htmxRetryConfig.maxRetries) { if (
(xhr.status === 0 || xhr.status >= 500) &&
currentRetries < htmxRetryConfig.maxRetries
) {
htmxRetryConfig.retryCount.set(retryKey, currentRetries + 1); htmxRetryConfig.retryCount.set(retryKey, currentRetries + 1);
const delay = htmxRetryConfig.retryDelay * Math.pow(2, currentRetries); const delay = htmxRetryConfig.retryDelay * Math.pow(2, currentRetries);
window.showNotification( window.showNotification(
`Request failed. Retrying in ${delay / 1000}s... (${currentRetries + 1}/${htmxRetryConfig.maxRetries})`, `Request failed. Retrying in ${delay / 1000}s... (${currentRetries + 1}/${htmxRetryConfig.maxRetries})`,
"warning", "warning",
delay delay,
); );
setTimeout(() => { setTimeout(() => {
@ -342,9 +387,11 @@ document.body.addEventListener("htmx:responseError", function (e) {
} else if (xhr.status === 404) { } else if (xhr.status === 404) {
errorMessage = "The requested content was not found."; errorMessage = "The requested content was not found.";
} else if (xhr.status >= 500) { } else if (xhr.status >= 500) {
errorMessage = "The server is experiencing issues. Please try again later."; errorMessage =
"The server is experiencing issues. Please try again later.";
} else if (xhr.status === 0) { } else if (xhr.status === 0) {
errorMessage = "Unable to connect. Please check your internet connection."; errorMessage =
"Unable to connect. Please check your internet connection.";
} }
if (target && target.id === "main-content") { if (target && target.id === "main-content") {
@ -365,10 +412,18 @@ document.body.addEventListener("htmx:afterRequest", function (e) {
// Handle timeout errors // Handle timeout errors
document.body.addEventListener("htmx:timeout", function (e) { document.body.addEventListener("htmx:timeout", function (e) {
window.showNotification("Request timed out. Please try again.", "warning", 5000); window.showNotification(
"Request timed out. Please try again.",
"warning",
5000,
);
}); });
// Handle send errors (network issues before request sent) // Handle send errors (network issues before request sent)
document.body.addEventListener("htmx:sendError", function (e) { document.body.addEventListener("htmx:sendError", function (e) {
window.showNotification("Network error. Please check your connection.", "error", 5000); window.showNotification(
"Network error. Please check your connection.",
"error",
5000,
);
}); });

View file

@ -1,13 +1,13 @@
// HTMX-based application initialization // HTMX-based application initialization
(function() { (function () {
'use strict'; "use strict";
// Configuration // Configuration
const config = { const config = {
wsUrl: '/ws', wsUrl: "/ws",
apiBase: '/api', apiBase: "/api",
reconnectDelay: 3000, reconnectDelay: 3000,
maxReconnectAttempts: 5 maxReconnectAttempts: 5,
}; };
// State // State
@ -17,81 +17,101 @@
// Initialize HTMX extensions // Initialize HTMX extensions
function initHTMX() { function initHTMX() {
// Configure HTMX // Configure HTMX
htmx.config.defaultSwapStyle = 'innerHTML'; htmx.config.defaultSwapStyle = "innerHTML";
htmx.config.defaultSettleDelay = 100; htmx.config.defaultSettleDelay = 100;
htmx.config.timeout = 10000; htmx.config.timeout = 10000;
// Add CSRF token to all requests if available // Add CSRF token to all requests if available
document.body.addEventListener('htmx:configRequest', (event) => { document.body.addEventListener("htmx:configRequest", (event) => {
const token = localStorage.getItem('csrf_token'); const token = localStorage.getItem("csrf_token");
if (token) { if (token) {
event.detail.headers['X-CSRF-Token'] = token; event.detail.headers["X-CSRF-Token"] = token;
} }
}); });
// Handle errors globally // Handle errors globally
document.body.addEventListener('htmx:responseError', (event) => { document.body.addEventListener("htmx:responseError", (event) => {
console.error('HTMX Error:', event.detail); console.error("HTMX Error:", event.detail);
showNotification('Connection error. Please try again.', 'error'); showNotification("Connection error. Please try again.", "error");
});
// Handle before swap to prevent errors when target doesn't exist
document.body.addEventListener("htmx:beforeSwap", (event) => {
const target = event.detail.target;
const status = event.detail.xhr?.status;
// If target doesn't exist or response is 404, prevent the swap
if (!target || status === 404) {
event.detail.shouldSwap = false;
return;
}
// For empty responses, set empty content to prevent insertBefore errors
if (
!event.detail.serverResponse ||
event.detail.serverResponse.trim() === ""
) {
event.detail.serverResponse = "<!-- empty -->";
}
}); });
// Handle successful swaps // Handle successful swaps
document.body.addEventListener('htmx:afterSwap', (event) => { document.body.addEventListener("htmx:afterSwap", (event) => {
// Auto-scroll messages if in chat // Auto-scroll messages if in chat
const messages = document.getElementById('messages'); const messages = document.getElementById("messages");
if (messages && event.detail.target === messages) { if (messages && event.detail.target === messages) {
messages.scrollTop = messages.scrollHeight; messages.scrollTop = messages.scrollHeight;
} }
}); });
// Handle WebSocket messages // Handle WebSocket messages
document.body.addEventListener('htmx:wsMessage', (event) => { document.body.addEventListener("htmx:wsMessage", (event) => {
handleWebSocketMessage(JSON.parse(event.detail.message)); handleWebSocketMessage(JSON.parse(event.detail.message));
}); });
// Handle WebSocket connection events // Handle WebSocket connection events
document.body.addEventListener('htmx:wsConnecting', () => { document.body.addEventListener("htmx:wsConnecting", () => {
updateConnectionStatus('connecting'); updateConnectionStatus("connecting");
}); });
document.body.addEventListener('htmx:wsOpen', () => { document.body.addEventListener("htmx:wsOpen", () => {
updateConnectionStatus('connected'); updateConnectionStatus("connected");
reconnectAttempts = 0; reconnectAttempts = 0;
}); });
document.body.addEventListener('htmx:wsClose', () => { document.body.addEventListener("htmx:wsClose", () => {
updateConnectionStatus('disconnected'); updateConnectionStatus("disconnected");
attemptReconnect(); attemptReconnect();
}); });
} }
// Handle WebSocket messages // Handle WebSocket messages
function handleWebSocketMessage(message) { function handleWebSocketMessage(message) {
switch(message.type) { switch (message.type) {
case 'message': case "message":
appendMessage(message); appendMessage(message);
break; break;
case 'notification': case "notification":
showNotification(message.text, message.severity); showNotification(message.text, message.severity);
break; break;
case 'status': case "status":
updateStatus(message); updateStatus(message);
break; break;
case 'suggestion': case "suggestion":
addSuggestion(message.text); addSuggestion(message.text);
break; break;
default: default:
console.log('Unknown message type:', message.type); console.log("Unknown message type:", message.type);
} }
} }
// Append message to chat // Append message to chat
function appendMessage(message) { function appendMessage(message) {
const messagesEl = document.getElementById('messages'); const messagesEl = document.getElementById("messages");
if (!messagesEl) return; if (!messagesEl) return;
const messageEl = document.createElement('div'); const messageEl = document.createElement("div");
messageEl.className = `message ${message.sender === 'user' ? 'user' : 'bot'}`; messageEl.className = `message ${message.sender === "user" ? "user" : "bot"}`;
messageEl.innerHTML = ` messageEl.innerHTML = `
<div class="message-content"> <div class="message-content">
<span class="sender">${message.sender}</span> <span class="sender">${message.sender}</span>
@ -106,16 +126,16 @@
// Add suggestion chip // Add suggestion chip
function addSuggestion(text) { function addSuggestion(text) {
const suggestionsEl = document.getElementById('suggestions'); const suggestionsEl = document.getElementById("suggestions");
if (!suggestionsEl) return; if (!suggestionsEl) return;
const chip = document.createElement('button'); const chip = document.createElement("button");
chip.className = 'suggestion-chip'; chip.className = "suggestion-chip";
chip.textContent = text; chip.textContent = text;
chip.setAttribute('hx-post', '/api/sessions/current/message'); chip.setAttribute("hx-post", "/api/sessions/current/message");
chip.setAttribute('hx-vals', JSON.stringify({content: text})); chip.setAttribute("hx-vals", JSON.stringify({ content: text }));
chip.setAttribute('hx-target', '#messages'); chip.setAttribute("hx-target", "#messages");
chip.setAttribute('hx-swap', 'beforeend'); chip.setAttribute("hx-swap", "beforeend");
suggestionsEl.appendChild(chip); suggestionsEl.appendChild(chip);
htmx.process(chip); htmx.process(chip);
@ -123,7 +143,7 @@
// Update connection status // Update connection status
function updateConnectionStatus(status) { function updateConnectionStatus(status) {
const statusEl = document.getElementById('connectionStatus'); const statusEl = document.getElementById("connectionStatus");
if (!statusEl) return; if (!statusEl) return;
statusEl.className = `connection-status ${status}`; statusEl.className = `connection-status ${status}`;
@ -132,7 +152,7 @@
// Update general status // Update general status
function updateStatus(message) { function updateStatus(message) {
const statusEl = document.getElementById('status-' + message.id); const statusEl = document.getElementById("status-" + message.id);
if (statusEl) { if (statusEl) {
statusEl.textContent = message.text; statusEl.textContent = message.text;
statusEl.className = `status ${message.severity}`; statusEl.className = `status ${message.severity}`;
@ -140,16 +160,16 @@
} }
// Show notification // Show notification
function showNotification(text, type = 'info') { function showNotification(text, type = "info") {
const notification = document.createElement('div'); const notification = document.createElement("div");
notification.className = `notification ${type}`; notification.className = `notification ${type}`;
notification.textContent = text; notification.textContent = text;
const container = document.getElementById('notifications') || document.body; const container = document.getElementById("notifications") || document.body;
container.appendChild(notification); container.appendChild(notification);
setTimeout(() => { setTimeout(() => {
notification.classList.add('fade-out'); notification.classList.add("fade-out");
setTimeout(() => notification.remove(), 300); setTimeout(() => notification.remove(), 300);
}, 3000); }, 3000);
} }
@ -157,86 +177,86 @@
// Attempt to reconnect WebSocket // Attempt to reconnect WebSocket
function attemptReconnect() { function attemptReconnect() {
if (reconnectAttempts >= config.maxReconnectAttempts) { if (reconnectAttempts >= config.maxReconnectAttempts) {
showNotification('Connection lost. Please refresh the page.', 'error'); showNotification("Connection lost. Please refresh the page.", "error");
return; return;
} }
reconnectAttempts++; reconnectAttempts++;
setTimeout(() => { setTimeout(() => {
console.log(`Reconnection attempt ${reconnectAttempts}...`); console.log(`Reconnection attempt ${reconnectAttempts}...`);
htmx.trigger(document.body, 'htmx:wsReconnect'); htmx.trigger(document.body, "htmx:wsReconnect");
}, config.reconnectDelay); }, config.reconnectDelay);
} }
// Utility: Escape HTML // Utility: Escape HTML
function escapeHtml(text) { function escapeHtml(text) {
const div = document.createElement('div'); const div = document.createElement("div");
div.textContent = text; div.textContent = text;
return div.innerHTML; return div.innerHTML;
} }
// Utility: Format timestamp // Utility: Format timestamp
function formatTime(timestamp) { function formatTime(timestamp) {
if (!timestamp) return ''; if (!timestamp) return "";
const date = new Date(timestamp); const date = new Date(timestamp);
return date.toLocaleTimeString('en-US', { return date.toLocaleTimeString("en-US", {
hour: 'numeric', hour: "numeric",
minute: '2-digit', minute: "2-digit",
hour12: true hour12: true,
}); });
} }
// Handle navigation // Handle navigation
function initNavigation() { function initNavigation() {
// Update active nav item on page change // Update active nav item on page change
document.addEventListener('htmx:pushedIntoHistory', (event) => { document.addEventListener("htmx:pushedIntoHistory", (event) => {
const path = event.detail.path; const path = event.detail.path;
updateActiveNav(path); updateActiveNav(path);
}); });
// Handle browser back/forward // Handle browser back/forward
window.addEventListener('popstate', (event) => { window.addEventListener("popstate", (event) => {
updateActiveNav(window.location.pathname); updateActiveNav(window.location.pathname);
}); });
} }
// Update active navigation item // Update active navigation item
function updateActiveNav(path) { function updateActiveNav(path) {
document.querySelectorAll('.nav-item, .app-item').forEach(item => { document.querySelectorAll(".nav-item, .app-item").forEach((item) => {
const href = item.getAttribute('href'); const href = item.getAttribute("href");
if (href === path || (path === '/' && href === '/chat')) { if (href === path || (path === "/" && href === "/chat")) {
item.classList.add('active'); item.classList.add("active");
} else { } else {
item.classList.remove('active'); item.classList.remove("active");
} }
}); });
} }
// Initialize keyboard shortcuts // Initialize keyboard shortcuts
function initKeyboardShortcuts() { function initKeyboardShortcuts() {
document.addEventListener('keydown', (e) => { document.addEventListener("keydown", (e) => {
// Send message on Enter (when in input) // Send message on Enter (when in input)
if (e.key === 'Enter' && !e.shiftKey) { if (e.key === "Enter" && !e.shiftKey) {
const input = document.getElementById('messageInput'); const input = document.getElementById("messageInput");
if (input && document.activeElement === input) { if (input && document.activeElement === input) {
e.preventDefault(); e.preventDefault();
const form = input.closest('form'); const form = input.closest("form");
if (form) { if (form) {
htmx.trigger(form, 'submit'); htmx.trigger(form, "submit");
} }
} }
} }
// Focus input on / // Focus input on /
if (e.key === '/' && document.activeElement.tagName !== 'INPUT') { if (e.key === "/" && document.activeElement.tagName !== "INPUT") {
e.preventDefault(); e.preventDefault();
const input = document.getElementById('messageInput'); const input = document.getElementById("messageInput");
if (input) input.focus(); if (input) input.focus();
} }
// Escape to blur input // Escape to blur input
if (e.key === 'Escape') { if (e.key === "Escape") {
const input = document.getElementById('messageInput'); const input = document.getElementById("messageInput");
if (input && document.activeElement === input) { if (input && document.activeElement === input) {
input.blur(); input.blur();
} }
@ -246,21 +266,23 @@
// Initialize scroll behavior // Initialize scroll behavior
function initScrollBehavior() { function initScrollBehavior() {
const scrollBtn = document.getElementById('scrollToBottom'); const scrollBtn = document.getElementById("scrollToBottom");
const messages = document.getElementById('messages'); const messages = document.getElementById("messages");
if (scrollBtn && messages) { if (scrollBtn && messages) {
// Show/hide scroll button // Show/hide scroll button
messages.addEventListener('scroll', () => { messages.addEventListener("scroll", () => {
const isAtBottom = messages.scrollHeight - messages.scrollTop <= messages.clientHeight + 100; const isAtBottom =
scrollBtn.style.display = isAtBottom ? 'none' : 'flex'; messages.scrollHeight - messages.scrollTop <=
messages.clientHeight + 100;
scrollBtn.style.display = isAtBottom ? "none" : "flex";
}); });
// Scroll to bottom on click // Scroll to bottom on click
scrollBtn.addEventListener('click', () => { scrollBtn.addEventListener("click", () => {
messages.scrollTo({ messages.scrollTo({
top: messages.scrollHeight, top: messages.scrollHeight,
behavior: 'smooth' behavior: "smooth",
}); });
}); });
} }
@ -275,7 +297,7 @@
// Main initialization // Main initialization
function init() { function init() {
console.log('Initializing HTMX application...'); console.log("Initializing HTMX application...");
// Initialize HTMX // Initialize HTMX
initHTMX(); initHTMX();
@ -295,12 +317,12 @@
// Set initial active nav // Set initial active nav
updateActiveNav(window.location.pathname); updateActiveNav(window.location.pathname);
console.log('HTMX application initialized'); console.log("HTMX application initialized");
} }
// Wait for DOM and HTMX to be ready // Wait for DOM and HTMX to be ready
if (document.readyState === 'loading') { if (document.readyState === "loading") {
document.addEventListener('DOMContentLoaded', init); document.addEventListener("DOMContentLoaded", init);
} else { } else {
init(); init();
} }
@ -310,6 +332,6 @@
showNotification, showNotification,
appendMessage, appendMessage,
updateConnectionStatus, updateConnectionStatus,
config config,
}; };
})(); })();

View file

@ -12,7 +12,7 @@
<!-- Folder List --> <!-- Folder List -->
<div class="folders-section"> <div class="folders-section">
<div class="nav-item active" data-folder="inbox" hx-get="/api/email/list?folder=inbox" hx-target="#mail-list" hx-swap="innerHTML"> <div class="nav-item active" data-folder="inbox" hx-get="/ui/email/list?folder=inbox" hx-target="#mail-list" hx-swap="innerHTML">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polyline points="22 12 16 12 14 15 10 15 8 12 2 12"/> <polyline points="22 12 16 12 14 15 10 15 8 12 2 12"/>
<path d="M5.45 5.11L2 12v6a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-6l-3.45-6.89A2 2 0 0 0 16.76 4H7.24a2 2 0 0 0-1.79 1.11z"/> <path d="M5.45 5.11L2 12v6a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-6l-3.45-6.89A2 2 0 0 0 16.76 4H7.24a2 2 0 0 0-1.79 1.11z"/>
@ -20,20 +20,20 @@
<span>Inbox</span> <span>Inbox</span>
<span class="folder-badge unread" id="inbox-count">0</span> <span class="folder-badge unread" id="inbox-count">0</span>
</div> </div>
<div class="nav-item" data-folder="starred" hx-get="/api/email/list?folder=starred" hx-target="#mail-list" hx-swap="innerHTML"> <div class="nav-item" data-folder="starred" hx-get="/ui/email/list?folder=starred" hx-target="#mail-list" hx-swap="innerHTML">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"/> <polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"/>
</svg> </svg>
<span>Starred</span> <span>Starred</span>
</div> </div>
<div class="nav-item" data-folder="sent" hx-get="/api/email/list?folder=sent" hx-target="#mail-list" hx-swap="innerHTML"> <div class="nav-item" data-folder="sent" hx-get="/ui/email/list?folder=sent" hx-target="#mail-list" hx-swap="innerHTML">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<line x1="22" y1="2" x2="11" y2="13"/> <line x1="22" y1="2" x2="11" y2="13"/>
<polygon points="22 2 15 22 11 13 2 9 22 2"/> <polygon points="22 2 15 22 11 13 2 9 22 2"/>
</svg> </svg>
<span>Sent</span> <span>Sent</span>
</div> </div>
<div class="nav-item" data-folder="scheduled" hx-get="/api/email/list?folder=scheduled" hx-target="#mail-list" hx-swap="innerHTML"> <div class="nav-item" data-folder="scheduled" hx-get="/ui/email/list?folder=scheduled" hx-target="#mail-list" hx-swap="innerHTML">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="12" cy="12" r="10"/> <circle cx="12" cy="12" r="10"/>
<polyline points="12 6 12 12 16 14"/> <polyline points="12 6 12 12 16 14"/>
@ -41,7 +41,7 @@
<span>Scheduled</span> <span>Scheduled</span>
<span class="folder-badge" id="scheduled-count">0</span> <span class="folder-badge" id="scheduled-count">0</span>
</div> </div>
<div class="nav-item" data-folder="drafts" hx-get="/api/email/list?folder=drafts" hx-target="#mail-list" hx-swap="innerHTML"> <div class="nav-item" data-folder="drafts" hx-get="/ui/email/list?folder=drafts" hx-target="#mail-list" hx-swap="innerHTML">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/> <path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/>
<polyline points="14 2 14 8 20 8"/> <polyline points="14 2 14 8 20 8"/>
@ -58,7 +58,7 @@
</svg> </svg>
<span>Tracking</span> <span>Tracking</span>
</div> </div>
<div class="nav-item" data-folder="spam" hx-get="/api/email/list?folder=spam" hx-target="#mail-list" hx-swap="innerHTML"> <div class="nav-item" data-folder="spam" hx-get="/ui/email/list?folder=spam" hx-target="#mail-list" hx-swap="innerHTML">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"/> <path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"/>
<line x1="12" y1="9" x2="12" y2="13"/> <line x1="12" y1="9" x2="12" y2="13"/>
@ -66,7 +66,7 @@
</svg> </svg>
<span>Spam</span> <span>Spam</span>
</div> </div>
<div class="nav-item" data-folder="trash" hx-get="/api/email/list?folder=trash" hx-target="#mail-list" hx-swap="innerHTML"> <div class="nav-item" data-folder="trash" hx-get="/ui/email/list?folder=trash" hx-target="#mail-list" hx-swap="innerHTML">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polyline points="3 6 5 6 21 6"/> <polyline points="3 6 5 6 21 6"/>
<path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/> <path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/>
@ -87,7 +87,7 @@
</button> </button>
</div> </div>
<div id="accounts-list" <div id="accounts-list"
hx-get="/api/email/accounts" hx-get="/ui/email/accounts"
hx-trigger="load" hx-trigger="load"
hx-swap="innerHTML"> hx-swap="innerHTML">
<!-- Accounts loaded here --> <!-- Accounts loaded here -->
@ -105,7 +105,7 @@
</svg> </svg>
</button> </button>
</div> </div>
<div id="labels-list" hx-get="/api/email/labels" hx-trigger="load" hx-swap="innerHTML"> <div id="labels-list" hx-get="/ui/email/labels" hx-trigger="load" hx-swap="innerHTML">
<div class="label-item" style="--label-color: #ef4444;"> <div class="label-item" style="--label-color: #ef4444;">
<span class="label-dot"></span> <span class="label-dot"></span>
<span>Important</span> <span>Important</span>
@ -169,7 +169,7 @@
<line x1="21" y1="21" x2="16.65" y2="16.65"/> <line x1="21" y1="21" x2="16.65" y2="16.65"/>
</svg> </svg>
<input type="text" placeholder="Search emails..." id="email-search" <input type="text" placeholder="Search emails..." id="email-search"
hx-get="/api/email/search" hx-trigger="keyup changed delay:300ms" hx-get="/ui/email/search" hx-trigger="keyup changed delay:300ms"
hx-target="#mail-list" hx-include="this" name="q"/> hx-target="#mail-list" hx-include="this" name="q"/>
</div> </div>
<button class="icon-btn" onclick="refreshMailList()" title="Refresh"> <button class="icon-btn" onclick="refreshMailList()" title="Refresh">
@ -212,7 +212,7 @@
</button> </button>
</div> </div>
<div id="mail-list" hx-get="/api/email/list?folder=inbox" hx-trigger="load" hx-swap="innerHTML"> <div id="mail-list" hx-get="/ui/email/list?folder=inbox" hx-trigger="load" hx-swap="innerHTML">
<div class="loading-state"> <div class="loading-state">
<div class="spinner"></div> <div class="spinner"></div>
<p>Loading emails...</p> <p>Loading emails...</p>
@ -421,7 +421,7 @@
</svg> </svg>
</button> </button>
</div> </div>
<div class="templates-list" id="templates-list" hx-get="/api/email/templates" hx-trigger="load" hx-swap="innerHTML"> <div class="templates-list" id="templates-list" hx-get="/ui/email/templates" hx-trigger="load" hx-swap="innerHTML">
<div class="loading-state"><div class="spinner"></div></div> <div class="loading-state"><div class="spinner"></div></div>
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
@ -443,7 +443,7 @@
</svg> </svg>
</button> </button>
</div> </div>
<div class="signatures-list" id="signatures-list" hx-get="/api/email/signatures" hx-trigger="load" hx-swap="innerHTML"> <div class="signatures-list" id="signatures-list" hx-get="/ui/email/signatures" hx-trigger="load" hx-swap="innerHTML">
<div class="loading-state"><div class="spinner"></div></div> <div class="loading-state"><div class="spinner"></div></div>
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
@ -465,7 +465,7 @@
</svg> </svg>
</button> </button>
</div> </div>
<div class="rules-list" id="rules-list" hx-get="/api/email/rules" hx-trigger="load" hx-swap="innerHTML"> <div class="rules-list" id="rules-list" hx-get="/ui/email/rules" hx-trigger="load" hx-swap="innerHTML">
<div class="loading-state"><div class="spinner"></div></div> <div class="loading-state"><div class="spinner"></div></div>
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
@ -487,7 +487,7 @@
</svg> </svg>
</button> </button>
</div> </div>
<form id="autoresponder-form" hx-post="/api/email/auto-responder" hx-swap="none"> <form id="autoresponder-form" hx-post="/ui/email/auto-responder" hx-swap="none">
<div class="form-group"> <div class="form-group">
<label class="toggle-label"> <label class="toggle-label">
<input type="checkbox" name="enabled" id="autoresponder-enabled"/> <input type="checkbox" name="enabled" id="autoresponder-enabled"/>
@ -1013,19 +1013,28 @@
to { transform: rotate(360deg); } to { transform: rotate(360deg); }
} }
/* Modals */ /* Modals - dialog elements are hidden by default */
.modal { dialog.modal {
position: fixed; position: fixed;
inset: 0; inset: 0;
background: rgba(0, 0, 0, 0.5); background: transparent;
border: none;
padding: 1rem;
z-index: 1000;
max-width: 100vw;
max-height: 100vh;
width: 100%;
height: 100%;
display: none;
}
dialog.modal[open] {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
z-index: 1000;
padding: 1rem;
} }
.modal::backdrop { dialog.modal::backdrop {
background: rgba(0, 0, 0, 0.5); background: rgba(0, 0, 0, 0.5);
} }

View file

@ -1,75 +1,78 @@
/* Logs page JavaScript */ /* Logs page JavaScript */
// Logs State // Logs State - guard against duplicate declarations on HTMX reload
let isStreaming = true; if (typeof window.logsModuleInitialized === "undefined") {
let autoScroll = true; window.logsModuleInitialized = true;
let logCounts = { debug: 0, info: 0, warn: 0, error: 0, fatal: 0 }; var isStreaming = true;
let searchDebounceTimer = null; var autoScroll = true;
let currentFilters = { var logCounts = { debug: 0, info: 0, warn: 0, error: 0, fatal: 0 };
level: 'all', var searchDebounceTimer = null;
service: 'all', var currentFilters = {
search: '' level: "all",
}; service: "all",
let logsWs = null; search: "",
};
var logsWs = null;
}
function initLogsWebSocket() { function initLogsWebSocket() {
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; const protocol = window.location.protocol === "https:" ? "wss:" : "ws:";
logsWs = new WebSocket(`${protocol}//${window.location.host}/ws/logs`); logsWs = new WebSocket(`${protocol}//${window.location.host}/ws/logs`);
logsWs.onopen = function() { logsWs.onopen = function () {
updateLogsConnectionStatus('connected', 'Connected'); updateLogsConnectionStatus("connected", "Connected");
}; };
logsWs.onclose = function() { logsWs.onclose = function () {
updateLogsConnectionStatus('disconnected', 'Disconnected'); updateLogsConnectionStatus("disconnected", "Disconnected");
// Reconnect after 3 seconds // Reconnect after 3 seconds
setTimeout(initLogsWebSocket, 3000); setTimeout(initLogsWebSocket, 3000);
}; };
logsWs.onerror = function() { logsWs.onerror = function () {
updateLogsConnectionStatus('disconnected', 'Error'); updateLogsConnectionStatus("disconnected", "Error");
}; };
logsWs.onmessage = function(event) { logsWs.onmessage = function (event) {
if (!isStreaming) return; if (!isStreaming) return;
try { try {
const logData = JSON.parse(event.data); const logData = JSON.parse(event.data);
appendLog(logData); appendLog(logData);
} catch (e) { } catch (e) {
console.error('Failed to parse log message:', e); console.error("Failed to parse log message:", e);
} }
}; };
} }
function updateLogsConnectionStatus(status, text) { function updateLogsConnectionStatus(status, text) {
const statusEl = document.getElementById('connection-status'); const statusEl = document.getElementById("connection-status");
if (statusEl) { if (statusEl) {
statusEl.className = `connection-status ${status}`; statusEl.className = `connection-status ${status}`;
statusEl.querySelector('.status-text').textContent = text; statusEl.querySelector(".status-text").textContent = text;
} }
} }
function appendLog(log) { function appendLog(log) {
const stream = document.getElementById('log-stream'); const stream = document.getElementById("log-stream");
if (!stream) return; if (!stream) return;
const placeholder = stream.querySelector('.log-placeholder'); const placeholder = stream.querySelector(".log-placeholder");
if (placeholder) { if (placeholder) {
placeholder.remove(); placeholder.remove();
} }
const entry = document.createElement('div'); const entry = document.createElement("div");
entry.className = 'log-entry'; entry.className = "log-entry";
entry.dataset.level = log.level || 'info'; entry.dataset.level = log.level || "info";
entry.dataset.service = log.service || 'unknown'; entry.dataset.service = log.service || "unknown";
entry.dataset.id = log.id || Date.now(); entry.dataset.id = log.id || Date.now();
entry.innerHTML = ` entry.innerHTML = `
<span class="log-timestamp">${formatLogTimestamp(log.timestamp)}</span> <span class="log-timestamp">${formatLogTimestamp(log.timestamp)}</span>
<span class="log-level">${(log.level || 'INFO').toUpperCase()}</span> <span class="log-level">${(log.level || "INFO").toUpperCase()}</span>
<span class="log-service">${log.service || 'unknown'}</span> <span class="log-service">${log.service || "unknown"}</span>
<span class="log-message">${escapeLogHtml(log.message || '')}</span> <span class="log-message">${escapeLogHtml(log.message || "")}</span>
<button class="log-expand" onclick="expandLog(this)" title="View details"> <button class="log-expand" onclick="expandLog(this)" title="View details">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polyline points="9 18 15 12 9 6"></polyline> <polyline points="9 18 15 12 9 6"></polyline>
@ -82,19 +85,19 @@ function appendLog(log) {
// Apply current filters // Apply current filters
if (!matchesLogFilters(entry)) { if (!matchesLogFilters(entry)) {
entry.classList.add('hidden'); entry.classList.add("hidden");
} }
stream.appendChild(entry); stream.appendChild(entry);
// Update counts // Update counts
const level = log.level || 'info'; const level = log.level || "info";
if (logCounts[level] !== undefined) { if (logCounts[level] !== undefined) {
logCounts[level]++; logCounts[level]++;
const countEl = document.getElementById(`${level}-count`); const countEl = document.getElementById(`${level}-count`);
if (countEl) countEl.textContent = logCounts[level]; if (countEl) countEl.textContent = logCounts[level];
} }
const totalEl = document.getElementById('total-count'); const totalEl = document.getElementById("total-count");
if (totalEl) { if (totalEl) {
totalEl.textContent = Object.values(logCounts).reduce((a, b) => a + b, 0); totalEl.textContent = Object.values(logCounts).reduce((a, b) => a + b, 0);
} }
@ -109,7 +112,7 @@ function appendLog(log) {
while (stream.children.length > maxEntries) { while (stream.children.length > maxEntries) {
const removed = stream.firstChild; const removed = stream.firstChild;
if (removed._logData) { if (removed._logData) {
const removedLevel = removed._logData.level || 'info'; const removedLevel = removed._logData.level || "info";
if (logCounts[removedLevel] > 0) { if (logCounts[removedLevel] > 0) {
logCounts[removedLevel]--; logCounts[removedLevel]--;
} }
@ -119,25 +122,31 @@ function appendLog(log) {
} }
function formatLogTimestamp(timestamp) { function formatLogTimestamp(timestamp) {
if (!timestamp) return '--'; if (!timestamp) return "--";
const date = new Date(timestamp); const date = new Date(timestamp);
return date.toISOString().replace('T', ' ').slice(0, 23); return date.toISOString().replace("T", " ").slice(0, 23);
} }
function escapeLogHtml(text) { function escapeLogHtml(text) {
const div = document.createElement('div'); const div = document.createElement("div");
div.textContent = text; div.textContent = text;
return div.innerHTML; return div.innerHTML;
} }
function matchesLogFilters(entry) { function matchesLogFilters(entry) {
// Level filter // Level filter
if (currentFilters.level !== 'all' && entry.dataset.level !== currentFilters.level) { if (
currentFilters.level !== "all" &&
entry.dataset.level !== currentFilters.level
) {
return false; return false;
} }
// Service filter // Service filter
if (currentFilters.service !== 'all' && entry.dataset.service !== currentFilters.service) { if (
currentFilters.service !== "all" &&
entry.dataset.service !== currentFilters.service
) {
return false; return false;
} }
@ -153,15 +162,17 @@ function matchesLogFilters(entry) {
} }
function applyLogFilters() { function applyLogFilters() {
currentFilters.level = document.getElementById('log-level-filter')?.value || 'all'; currentFilters.level =
currentFilters.service = document.getElementById('service-filter')?.value || 'all'; document.getElementById("log-level-filter")?.value || "all";
currentFilters.service =
document.getElementById("service-filter")?.value || "all";
const entries = document.querySelectorAll('.log-entry'); const entries = document.querySelectorAll(".log-entry");
entries.forEach(entry => { entries.forEach((entry) => {
if (matchesLogFilters(entry)) { if (matchesLogFilters(entry)) {
entry.classList.remove('hidden'); entry.classList.remove("hidden");
} else { } else {
entry.classList.add('hidden'); entry.classList.add("hidden");
} }
}); });
} }
@ -176,11 +187,11 @@ function debounceLogSearch(value) {
function toggleStream() { function toggleStream() {
isStreaming = !isStreaming; isStreaming = !isStreaming;
const btn = document.getElementById('stream-toggle'); const btn = document.getElementById("stream-toggle");
if (!btn) return; if (!btn) return;
if (isStreaming) { if (isStreaming) {
btn.classList.remove('paused'); btn.classList.remove("paused");
btn.innerHTML = ` btn.innerHTML = `
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<rect x="6" y="4" width="4" height="16"></rect> <rect x="6" y="4" width="4" height="16"></rect>
@ -189,7 +200,7 @@ function toggleStream() {
<span>Pause</span> <span>Pause</span>
`; `;
} else { } else {
btn.classList.add('paused'); btn.classList.add("paused");
btn.innerHTML = ` btn.innerHTML = `
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polygon points="5 3 19 12 5 21 5 3"></polygon> <polygon points="5 3 19 12 5 21 5 3"></polygon>
@ -200,8 +211,8 @@ function toggleStream() {
} }
function clearLogs() { function clearLogs() {
if (confirm('Are you sure you want to clear all logs?')) { if (confirm("Are you sure you want to clear all logs?")) {
const stream = document.getElementById('log-stream'); const stream = document.getElementById("log-stream");
if (!stream) return; if (!stream) return;
stream.innerHTML = ` stream.innerHTML = `
@ -220,30 +231,32 @@ function clearLogs() {
// Reset counts // Reset counts
logCounts = { debug: 0, info: 0, warn: 0, error: 0, fatal: 0 }; logCounts = { debug: 0, info: 0, warn: 0, error: 0, fatal: 0 };
Object.keys(logCounts).forEach(level => { Object.keys(logCounts).forEach((level) => {
const el = document.getElementById(`${level}-count`); const el = document.getElementById(`${level}-count`);
if (el) el.textContent = '0'; if (el) el.textContent = "0";
}); });
const totalEl = document.getElementById('total-count'); const totalEl = document.getElementById("total-count");
if (totalEl) totalEl.textContent = '0'; if (totalEl) totalEl.textContent = "0";
} }
} }
function downloadLogs() { function downloadLogs() {
const entries = document.querySelectorAll('.log-entry'); const entries = document.querySelectorAll(".log-entry");
let logs = []; let logs = [];
entries.forEach(entry => { entries.forEach((entry) => {
if (entry._logData) { if (entry._logData) {
logs.push(entry._logData); logs.push(entry._logData);
} }
}); });
const blob = new Blob([JSON.stringify(logs, null, 2)], { type: 'application/json' }); const blob = new Blob([JSON.stringify(logs, null, 2)], {
type: "application/json",
});
const url = URL.createObjectURL(blob); const url = URL.createObjectURL(blob);
const a = document.createElement('a'); const a = document.createElement("a");
a.href = url; a.href = url;
a.download = `logs-${new Date().toISOString().slice(0, 19).replace(/[T:]/g, '-')}.json`; a.download = `logs-${new Date().toISOString().slice(0, 19).replace(/[T:]/g, "-")}.json`;
document.body.appendChild(a); document.body.appendChild(a);
a.click(); a.click();
document.body.removeChild(a); document.body.removeChild(a);
@ -251,7 +264,7 @@ function downloadLogs() {
} }
function scrollToTop() { function scrollToTop() {
const stream = document.getElementById('log-stream'); const stream = document.getElementById("log-stream");
if (stream) { if (stream) {
stream.scrollTop = 0; stream.scrollTop = 0;
autoScroll = false; autoScroll = false;
@ -260,7 +273,7 @@ function scrollToTop() {
} }
function scrollToBottom() { function scrollToBottom() {
const stream = document.getElementById('log-stream'); const stream = document.getElementById("log-stream");
if (stream) { if (stream) {
stream.scrollTop = stream.scrollHeight; stream.scrollTop = stream.scrollHeight;
autoScroll = true; autoScroll = true;
@ -269,68 +282,76 @@ function scrollToBottom() {
} }
function updateLogScrollButtons() { function updateLogScrollButtons() {
const topBtn = document.getElementById('scroll-top-btn'); const topBtn = document.getElementById("scroll-top-btn");
const bottomBtn = document.getElementById('scroll-bottom-btn'); const bottomBtn = document.getElementById("scroll-bottom-btn");
if (topBtn) topBtn.classList.toggle('active', !autoScroll); if (topBtn) topBtn.classList.toggle("active", !autoScroll);
if (bottomBtn) bottomBtn.classList.toggle('active', autoScroll); if (bottomBtn) bottomBtn.classList.toggle("active", autoScroll);
} }
function expandLog(btn) { function expandLog(btn) {
const entry = btn.closest('.log-entry'); const entry = btn.closest(".log-entry");
const logData = entry._logData || { const logData = entry._logData || {
timestamp: entry.querySelector('.log-timestamp').textContent, timestamp: entry.querySelector(".log-timestamp").textContent,
level: entry.dataset.level, level: entry.dataset.level,
service: entry.dataset.service, service: entry.dataset.service,
message: entry.querySelector('.log-message').textContent message: entry.querySelector(".log-message").textContent,
}; };
const panel = document.getElementById('log-detail-panel'); const panel = document.getElementById("log-detail-panel");
const content = document.getElementById('log-detail-content'); const content = document.getElementById("log-detail-content");
if (!panel || !content) return; if (!panel || !content) return;
content.innerHTML = ` content.innerHTML = `
<div class="detail-section"> <div class="detail-section">
<div class="detail-label">Timestamp</div> <div class="detail-label">Timestamp</div>
<div class="detail-value">${logData.timestamp || '--'}</div> <div class="detail-value">${logData.timestamp || "--"}</div>
</div> </div>
<div class="detail-section"> <div class="detail-section">
<div class="detail-label">Level</div> <div class="detail-label">Level</div>
<div class="detail-value">${(logData.level || 'info').toUpperCase()}</div> <div class="detail-value">${(logData.level || "info").toUpperCase()}</div>
</div> </div>
<div class="detail-section"> <div class="detail-section">
<div class="detail-label">Service</div> <div class="detail-label">Service</div>
<div class="detail-value">${logData.service || 'unknown'}</div> <div class="detail-value">${logData.service || "unknown"}</div>
</div> </div>
<div class="detail-section"> <div class="detail-section">
<div class="detail-label">Message</div> <div class="detail-label">Message</div>
<div class="detail-value">${escapeLogHtml(logData.message || '')}</div> <div class="detail-value">${escapeLogHtml(logData.message || "")}</div>
</div> </div>
${logData.stack ? ` ${
logData.stack
? `
<div class="detail-section"> <div class="detail-section">
<div class="detail-label">Stack Trace</div> <div class="detail-label">Stack Trace</div>
<div class="detail-value">${escapeLogHtml(logData.stack)}</div> <div class="detail-value">${escapeLogHtml(logData.stack)}</div>
</div> </div>
` : ''} `
${logData.context ? ` : ""
}
${
logData.context
? `
<div class="detail-section"> <div class="detail-section">
<div class="detail-label">Context</div> <div class="detail-label">Context</div>
<div class="detail-value">${escapeLogHtml(JSON.stringify(logData.context, null, 2))}</div> <div class="detail-value">${escapeLogHtml(JSON.stringify(logData.context, null, 2))}</div>
</div> </div>
` : ''} `
: ""
}
`; `;
panel.classList.add('open'); panel.classList.add("open");
} }
function closeLogDetail() { function closeLogDetail() {
const panel = document.getElementById('log-detail-panel'); const panel = document.getElementById("log-detail-panel");
if (panel) panel.classList.remove('open'); if (panel) panel.classList.remove("open");
} }
// Initialize on page load // Initialize on page load
document.addEventListener('DOMContentLoaded', function() { document.addEventListener("DOMContentLoaded", function () {
// Initialize WebSocket connection if on logs page // Initialize WebSocket connection if on logs page
if (document.getElementById('log-stream')) { if (document.getElementById("log-stream")) {
initLogsWebSocket(); initLogsWebSocket();
} }
}); });

View file

@ -1,67 +1,75 @@
/* Monitoring module - shared/base JavaScript */ /* Monitoring module - shared/base JavaScript */
function setActiveNav(element) { function setActiveNav(element) {
document.querySelectorAll('.monitoring-nav .nav-item').forEach(item => { document.querySelectorAll(".monitoring-nav .nav-item").forEach((item) => {
item.classList.remove('active'); item.classList.remove("active");
}); });
element.classList.add('active'); element.classList.add("active");
// Update page title // Update page title
const title = element.querySelector('span:not(.alert-badge):not(.health-indicator)').textContent; const title = element.querySelector(
document.getElementById('page-title').textContent = title; "span:not(.alert-badge):not(.health-indicator)",
).textContent;
document.getElementById("page-title").textContent = title;
} }
function updateTimeRange(range) { function updateTimeRange(range) {
// Store selected time range // Store selected time range
localStorage.setItem('monitoring-time-range', range); localStorage.setItem("monitoring-time-range", range);
// Trigger refresh of current view // Trigger refresh of current view
htmx.trigger('#monitoring-content', 'refresh'); htmx.trigger("#monitoring-content", "refresh");
} }
function refreshMonitoring() { function refreshMonitoring() {
htmx.trigger('#monitoring-content', 'refresh'); htmx.trigger("#monitoring-content", "refresh");
// Visual feedback // Visual feedback
const btn = event.currentTarget; const btn = event.currentTarget;
btn.classList.add('active'); btn.classList.add("active");
setTimeout(() => btn.classList.remove('active'), 500); setTimeout(() => btn.classList.remove("active"), 500);
}
// Guard against duplicate declarations on HTMX reload
if (typeof window.monitoringModuleInitialized === "undefined") {
window.monitoringModuleInitialized = true;
var autoRefresh = true;
} }
let autoRefresh = true;
function toggleAutoRefresh() { function toggleAutoRefresh() {
autoRefresh = !autoRefresh; autoRefresh = !autoRefresh;
const btn = document.getElementById('auto-refresh-btn'); const btn = document.getElementById("auto-refresh-btn");
btn.classList.toggle('active', autoRefresh); btn.classList.toggle("active", autoRefresh);
if (autoRefresh) { if (autoRefresh) {
// Re-enable polling by refreshing the page content // Re-enable polling by refreshing the page content
htmx.trigger('#monitoring-content', 'refresh'); htmx.trigger("#monitoring-content", "refresh");
} }
} }
function exportData() { function exportData() {
const timeRange = document.getElementById('time-range').value; const timeRange = document.getElementById("time-range").value;
window.open(`/api/monitoring/export?range=${timeRange}`, '_blank'); window.open(`/api/monitoring/export?range=${timeRange}`, "_blank");
} }
// Initialize // Initialize
document.addEventListener('DOMContentLoaded', function() { document.addEventListener("DOMContentLoaded", function () {
// Restore time range preference // Restore time range preference
const savedRange = localStorage.getItem('monitoring-time-range'); const savedRange = localStorage.getItem("monitoring-time-range");
if (savedRange) { if (savedRange) {
const timeRangeEl = document.getElementById('time-range'); const timeRangeEl = document.getElementById("time-range");
if (timeRangeEl) timeRangeEl.value = savedRange; if (timeRangeEl) timeRangeEl.value = savedRange;
} }
// Set auto-refresh button state // Set auto-refresh button state
const autoRefreshBtn = document.getElementById('auto-refresh-btn'); const autoRefreshBtn = document.getElementById("auto-refresh-btn");
if (autoRefreshBtn) autoRefreshBtn.classList.toggle('active', autoRefresh); if (autoRefreshBtn) autoRefreshBtn.classList.toggle("active", autoRefresh);
}); });
// Handle HTMX events for loading states // Handle HTMX events for loading states
document.body.addEventListener('htmx:beforeRequest', function(evt) { document.body.addEventListener("htmx:beforeRequest", function (evt) {
if (evt.target.id === 'monitoring-content') { if (evt.target.id === "monitoring-content") {
evt.target.innerHTML = '<div class="loading-state"><div class="spinner"></div><p>Loading...</p></div>'; evt.target.innerHTML =
'<div class="loading-state"><div class="spinner"></div><p>Loading...</p></div>';
} }
}); });

View file

@ -6,9 +6,10 @@
/* Research Container */ /* Research Container */
.research-container { .research-container {
display: flex; display: flex;
height: calc(100vh - 60px); height: 100%;
background: var(--background); min-height: 0;
color: var(--foreground); background: var(--background, var(--bg-primary, #0a0a0f));
color: var(--foreground, var(--text-primary, #ffffff));
} }
/* Sidebar */ /* Sidebar */
@ -398,12 +399,22 @@
animation: bounce 1.4s infinite ease-in-out both; animation: bounce 1.4s infinite ease-in-out both;
} }
.indicator-dots span:nth-child(1) { animation-delay: -0.32s; } .indicator-dots span:nth-child(1) {
.indicator-dots span:nth-child(2) { animation-delay: -0.16s; } animation-delay: -0.32s;
}
.indicator-dots span:nth-child(2) {
animation-delay: -0.16s;
}
@keyframes bounce { @keyframes bounce {
0%, 80%, 100% { transform: scale(0); } 0%,
40% { transform: scale(1); } 80%,
100% {
transform: scale(0);
}
40% {
transform: scale(1);
}
} }
.indicator-text { .indicator-text {
@ -557,7 +568,7 @@
background: var(--muted); background: var(--muted);
padding: 2px 6px; padding: 2px 6px;
border-radius: 4px; border-radius: 4px;
font-family: 'JetBrains Mono', 'Fira Code', monospace; font-family: "JetBrains Mono", "Fira Code", monospace;
font-size: 13px; font-size: 13px;
} }

379
ui/suite/tasks/intents.html Normal file
View file

@ -0,0 +1,379 @@
<!-- Intents Dashboard - Sentient Theme -->
<!-- AI-powered intent tracking and decision management -->
<div class="intents-dashboard">
<!-- Dashboard Header -->
<div class="dashboard-header">
<div class="dashboard-title">
<h1>Dashboard</h1>
<span class="dashboard-subtitle">Analytics</span>
</div>
<div class="dashboard-actions">
<div class="search-bar">
<svg
class="search-bar-icon"
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<circle cx="11" cy="11" r="8" />
<path d="m21 21-4.35-4.35" />
</svg>
<input
type="text"
class="search-bar-input"
placeholder="Search Intents"
/>
</div>
<div class="profile-selector">
<div class="profile-avatar">A</div>
<span class="profile-name">Profile 1</span>
<svg
class="profile-dropdown-icon"
width="12"
height="12"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<polyline points="6 9 12 15 18 9" />
</svg>
</div>
<div class="notification-indicator">
<svg
width="18"
height="18"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<path d="M18 8A6 6 0 0 0 6 8c0 7-3 9-3 9h18s-3-2-3-9" />
<path d="M13.73 21a2 2 0 0 1-3.46 0" />
</svg>
<span class="notification-count">3</span>
</div>
<button class="btn-new-intent">
<svg
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<line x1="12" y1="5" x2="12" y2="19" />
<line x1="5" y1="12" x2="19" y2="12" />
</svg>
New Intent
</button>
<button class="icon-button" title="Settings">
<svg
width="18"
height="18"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<circle cx="12" cy="12" r="3" />
<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"
/>
</svg>
</button>
</div>
</div>
<!-- Status Filters -->
<div class="status-filters">
<button class="status-filter active">
<svg
class="icon"
width="14"
height="14"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<polyline points="20 6 9 17 4 12" />
</svg>
Complete
<span class="count">8</span>
</button>
<button class="status-filter">
<svg
class="icon"
width="14"
height="14"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<circle cx="12" cy="12" r="10" />
<polygon points="10 8 16 12 10 16 10 8" />
</svg>
Active Intents
<span class="count">12</span>
</button>
<button class="status-filter">
<svg
class="icon"
width="14"
height="14"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<rect x="3" y="3" width="18" height="18" rx="2" ry="2" />
<line x1="9" y1="9" x2="15" y2="15" />
<line x1="15" y1="9" x2="9" y2="15" />
</svg>
Awaiting Decision
<span class="count">5</span>
</button>
<button class="status-filter">
<svg
class="icon"
width="14"
height="14"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<rect x="6" y="4" width="4" height="16" />
<rect x="14" y="4" width="4" height="16" />
</svg>
Paused
<span class="count">2</span>
</button>
<button class="status-filter">
<svg
class="icon"
width="14"
height="14"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<path
d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"
/>
<line x1="12" y1="9" x2="12" y2="13" />
<line x1="12" y1="17" x2="12.01" y2="17" />
</svg>
Blocked/Issues
<span class="count">1</span>
</button>
<div class="time-saved-indicator">
<svg
class="icon"
width="14"
height="14"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<circle cx="12" cy="12" r="10" />
<polyline points="12 6 12 12 16 14" />
</svg>
Active Time Saved: 23.5 hrs this week
</div>
</div>
<!-- Main Content Area -->
<div class="dashboard-layout">
<!-- Left Column: Intent Cards -->
<div class="dashboard-main">
<div class="intents-grid">
<!-- Intent Card 1: Active -->
<article class="intent-card">
<div class="intent-card-header">
<h3 class="intent-title">
Launch Q4 Campaign for Verse
</h3>
</div>
<div class="intent-progress">
<div class="progress-bar">
<div
class="progress-bar-fill"
style="width: 75%"
></div>
</div>
<span class="progress-text">5/7 Steps</span>
<span class="progress-text">75%</span>
</div>
<div class="intent-status-line">
<span class="intent-status-label">STATUS</span>
<span class="intent-status-badge active">
<svg
width="10"
height="10"
viewBox="0 0 24 24"
fill="currentColor"
>
<circle cx="12" cy="12" r="6" />
</svg>
Active
</span>
</div>
<div class="intent-description">
<strong>Budget Allocation: 50k</strong><br />
<span class="pending"
>Distributing budget in media channels</span
>
<span
class="intent-meta-tag"
style="margin-left: 8px; display: inline-flex"
>Est: 15min ⏱</span
>
</div>
<div class="intent-status-line" style="margin-top: 16px">
<span class="intent-status-label">INTEGRITY</span>
</div>
<div class="intent-meta">
<span class="intent-meta-tag">Started 2h ago</span>
<span class="intent-meta-tag">Due Nov 15th</span>
<span class="intent-health good"
>Intent Health 90%</span
>
</div>
<a href="#" class="intent-detailed-link">Detailed View</a>
</article>
<!-- Intent Card 2: Awaiting Decision -->
<article class="intent-card selected">
<div class="intent-card-header">
<h3 class="intent-title">
Make a financial CRM for Deloitte
</h3>
</div>
<div class="intent-progress">
<div class="progress-bar">
<div
class="progress-bar-fill warning"
style="width: 45%"
></div>
</div>
<span class="progress-text warning">25/60 Steps</span>
<span class="progress-text warning">45%</span>
</div>
<div class="intent-status-line">
<span class="intent-status-label">STATUS</span>
<span class="intent-status-badge awaiting">
<svg
width="10"
height="10"
viewBox="0 0 24 24"
fill="currentColor"
>
<rect
x="3"
y="3"
width="18"
height="18"
rx="2"
/>
</svg>
Awaiting Decision
</span>
</div>
<div class="intent-description">
<strong>Implement User Authentication System</strong
><br />
<span class="pending"
>The authentication system needs to balance
security...</span
>
<span
class="intent-meta-tag"
style="margin-left: 8px; display: inline-flex"
>Pending ⏸</span
>
</div>
<div class="intent-status-line" style="margin-top: 16px">
<span class="intent-status-label">INTEGRITY</span>
</div>
<div class="intent-meta">
<span class="intent-meta-tag">Started 5d ago</span>
<span class="intent-meta-tag">Due Nov 30th</span>
<span class="intent-health warning"
>Intent Health 85%</span
>
</div>
</article>
</div>
</div>
<!-- Right Column: Detail Panel -->
<div class="dashboard-sidebar">
<div class="detail-panel">
<!-- Detail Header -->
<div class="detail-panel-header">
<div class="detail-panel-nav">
<button class="nav-btn" title="Previous">
<svg
width="14"
height="14"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<polyline points="15 18 9 12 15 6" />
</svg>
</button>
</div>
<h2>Make a financial CRM for Deloitte</h2>
<div class="detail-panel-actions">
<span class="intent-status-badge awaiting">
<svg
width="10"
height="10"
viewBox="0 0 24 24"
fill="currentColor"
>
<rect
x="3"
y="3"
width="18"
height="18"
rx="2"
/>
</svg>
Awaiting Decision
</span>
<button class="action-btn-pause" title="Pause Intent">
<svg
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<rect x="6" y="4" width="4" height="16" />
<rect x="14" y="4" width="4" height="16" />
</svg>
</button>
</div>
</div>
<!-- Progress Bar in Header -->
<div class="detail-status">
<div class="detail-progress-bar" style="flex: 1"></div>
</div>
</div>
</div>
</div>
</div>

View file

@ -10,10 +10,14 @@
.tasks-app { .tasks-app {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
min-height: 100vh; height: 100%;
background: var(--sentient-bg-primary); min-height: 0;
color: var(--sentient-text-primary); background: var(--bg-primary, var(--sentient-bg-primary, #0a0a0f));
font-family: var(--sentient-font-family); color: var(--text-primary, var(--sentient-text-primary, #ffffff));
font-family: var(
--font-family,
var(--sentient-font-family, system-ui, -apple-system, sans-serif)
);
} }
/* ============================================================================= /* =============================================================================
@ -25,8 +29,35 @@
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
padding: 16px 24px; padding: 16px 24px;
background: var(--sentient-bg-secondary); background: var(--bg-secondary, var(--sentient-bg-secondary, #12121a));
border-bottom: 1px solid var(--sentient-border); border-bottom: 1px solid var(--border, var(--sentient-border, #2a2a3a));
}
/* New Intent Button */
.btn-new-intent {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 10px 20px;
background: var(--accent, var(--sentient-accent, #d4f505));
color: var(--accent-foreground, #000000);
border: none;
border-radius: 8px;
font-size: 14px;
font-weight: 600;
cursor: pointer;
transition: all 0.2s ease;
margin-left: auto;
}
.btn-new-intent:hover {
background: var(--accent-hover, var(--sentient-accent-hover, #bfdd04));
box-shadow: 0 0 20px var(--accent-glow, rgba(212, 245, 5, 0.3));
}
.btn-new-intent svg {
width: 16px;
height: 16px;
} }
.topbar-left { .topbar-left {
@ -44,6 +75,18 @@
color: var(--sentient-text-primary); color: var(--sentient-text-primary);
} }
/* Tasks List Title Row */
.tasks-list-title {
display: flex;
align-items: center;
gap: 16px;
flex-wrap: wrap;
}
.tasks-list-title h1 {
margin: 0;
}
.topbar-logo-icon { .topbar-logo-icon {
width: 32px; width: 32px;
height: 32px; height: 32px;
@ -211,6 +254,7 @@
display: flex; display: flex;
flex: 1; flex: 1;
overflow: hidden; overflow: hidden;
min-height: 0;
} }
/* ============================================================================= /* =============================================================================
@ -221,13 +265,17 @@
flex: 1; flex: 1;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
border-right: 1px solid var(--sentient-border); border-right: 1px solid var(--border, var(--sentient-border, #2a2a3a));
overflow: hidden; overflow: hidden;
min-height: 0;
} }
.tasks-list-header { .tasks-list-header {
padding: 20px 24px; padding: 20px 24px;
border-bottom: 1px solid var(--sentient-border); border-bottom: 1px solid var(--border, var(--sentient-border, #2a2a3a));
flex-shrink: 0;
position: relative;
z-index: 1;
} }
.tasks-list-title { .tasks-list-title {
@ -240,13 +288,13 @@
.tasks-list-title h1 { .tasks-list-title h1 {
font-size: 24px; font-size: 24px;
font-weight: 700; font-weight: 700;
color: var(--sentient-text-primary); color: var(--text-primary, var(--sentient-text-primary, #ffffff));
margin: 0; margin: 0;
} }
.tasks-count { .tasks-count {
font-size: 14px; font-size: 14px;
color: var(--sentient-text-muted); color: var(--text-muted, var(--sentient-text-muted, #6b6b80));
} }
/* Status Filter Pills */ /* Status Filter Pills */
@ -254,6 +302,8 @@
display: flex; display: flex;
gap: 8px; gap: 8px;
flex-wrap: wrap; flex-wrap: wrap;
position: relative;
z-index: 1;
} }
.status-pill { .status-pill {

View file

@ -14,6 +14,24 @@
<span class="tasks-count" id="tasks-total-count" <span class="tasks-count" id="tasks-total-count"
>24 tasks</span >24 tasks</span
> >
<button
class="btn-new-intent"
onclick="showNewIntentModal()"
title="Create new intent"
>
<svg
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<line x1="12" y1="5" x2="12" y2="19" />
<line x1="5" y1="12" x2="19" y2="12" />
</svg>
New Intent
</button>
</div> </div>
<!-- Status Filter Pills --> <!-- Status Filter Pills -->
@ -505,4 +523,156 @@
</main> </main>
</div> </div>
<script src="/suite/tasks/tasks.js"></script> <!-- New Intent Modal -->
<div class="modal-overlay" id="newIntentModal">
<div class="modal">
<div class="modal-header">
<h3>Create New Intent</h3>
<button class="modal-close" onclick="closeNewIntentModal()">
<svg
width="20"
height="20"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<line x1="18" y1="6" x2="6" y2="18" />
<line x1="6" y1="6" x2="18" y2="18" />
</svg>
</button>
</div>
<div class="modal-body">
<form
id="newIntentForm"
hx-post="/api/tasks"
hx-target="#task-list"
hx-swap="afterbegin"
>
<div class="form-group">
<label class="form-label" for="intentTitle"
>Intent Title</label
>
<input
type="text"
class="form-input"
id="intentTitle"
name="title"
placeholder="e.g., Launch Q4 Campaign for Verse"
required
/>
</div>
<div class="form-group">
<label class="form-label" for="intentDescription"
>Description</label
>
<textarea
class="form-input"
id="intentDescription"
name="description"
rows="3"
placeholder="Describe what this intent should accomplish..."
></textarea>
</div>
<div class="form-row">
<div class="form-group">
<label class="form-label" for="intentDueDate"
>Due Date</label
>
<input
type="date"
class="form-input"
id="intentDueDate"
name="due_date"
/>
</div>
<div class="form-group">
<label class="form-label" for="intentPriority"
>Priority</label
>
<select
class="form-input"
id="intentPriority"
name="priority"
>
<option value="normal">Normal</option>
<option value="high">High</option>
<option value="urgent">Urgent</option>
</select>
</div>
</div>
<div class="form-group">
<label class="form-label" for="intentGoal"
>Goal / Success Criteria</label
>
<textarea
class="form-input"
id="intentGoal"
name="goal"
rows="2"
placeholder="How will you know when this intent is complete?"
></textarea>
</div>
</form>
</div>
<div class="modal-footer">
<button
class="decision-btn decision-btn-secondary"
onclick="closeNewIntentModal()"
>
Cancel
</button>
<button
class="decision-btn decision-btn-primary"
type="submit"
form="newIntentForm"
>
Create Intent
</button>
</div>
</div>
</div>
<script>
// Conditionally load tasks.js only if not already loaded
if (typeof TasksState === "undefined") {
const script = document.createElement("script");
script.src = "tasks/tasks.js";
document.head.appendChild(script);
}
</script>
<script>
// New Intent Modal functions
function showNewIntentModal() {
document.getElementById("newIntentModal").classList.add("show");
document.getElementById("intentTitle").focus();
}
function closeNewIntentModal() {
document.getElementById("newIntentModal").classList.remove("show");
document.getElementById("newIntentForm").reset();
}
// Close modal on escape key
document.addEventListener("keydown", (e) => {
if (e.key === "Escape") {
closeNewIntentModal();
}
});
// Close modal when clicking overlay
document
.getElementById("newIntentModal")
?.addEventListener("click", (e) => {
if (e.target.classList.contains("modal-overlay")) {
closeNewIntentModal();
}
});
// Close modal after successful form submission
document.body.addEventListener("htmx:afterRequest", (e) => {
if (e.detail.elt.id === "newIntentForm" && e.detail.successful) {
closeNewIntentModal();
}
});
</script>

View file

@ -7,13 +7,16 @@
// STATE MANAGEMENT // STATE MANAGEMENT
// ============================================================================= // =============================================================================
const TasksState = { // Prevent duplicate declaration when script is reloaded via HTMX
if (typeof TasksState === "undefined") {
var TasksState = {
selectedTaskId: 2, // Default selected task selectedTaskId: 2, // Default selected task
currentFilter: "complete", currentFilter: "complete",
tasks: [], tasks: [],
wsConnection: null, wsConnection: null,
agentLogPaused: false, agentLogPaused: false,
}; };
}
// ============================================================================= // =============================================================================
// INITIALIZATION // INITIALIZATION