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 std::{fs, path::PathBuf};
use tokio_tungstenite::{
connect_async_tls_with_config,
tungstenite::protocol::Message as TungsteniteMessage,
connect_async_tls_with_config, tungstenite::protocol::Message as TungsteniteMessage,
};
use crate::shared::AppState;
@ -99,7 +98,10 @@ async fn proxy_api(
req: Request<Body>,
) -> Response<Body> {
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 headers = req.headers().clone();
@ -217,7 +219,11 @@ async fn ws_proxy(
async fn handle_ws_proxy(client_socket: WebSocket, state: AppState, params: WsQuery) {
let backend_url = format!(
"{}/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.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);
// Connect to backend WebSocket
let backend_result = connect_async_tls_with_config(
&backend_url,
None,
false,
Some(connector),
).await;
let backend_result =
connect_async_tls_with_config(&backend_url, None, false, Some(connector)).await;
let backend_socket = match backend_result {
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 {
match msg {
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;
}
}
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;
}
}
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;
}
}
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;
}
}
@ -323,8 +341,32 @@ async fn handle_ws_proxy(client_socket: WebSocket, state: AppState, params: WsQu
/// Create WebSocket proxy router
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()
.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
@ -338,6 +380,8 @@ pub fn configure_router() -> Router {
.route("/health", get(health))
// API proxy routes
.nest("/api", create_api_router())
// UI HTMX proxy routes (for /ui/* endpoints that return HTML fragments)
.nest("/ui", create_ui_router())
// WebSocket proxy routes
.nest("/ws", create_ws_router())
// UI routes
@ -373,6 +417,62 @@ pub fn configure_router() -> Router {
"/suite/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)
.nest_service(
"/js",

View file

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

View file

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

View file

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

View file

@ -3,9 +3,10 @@
/* Calendar Container */
.calendar-container {
display: flex;
height: calc(100vh - 60px);
background: var(--background);
color: var(--foreground);
height: 100%;
min-height: 0;
background: var(--background, var(--bg-primary, #0a0a0f));
color: var(--foreground, var(--text-primary, #ffffff));
}
/* Sidebar */
@ -135,7 +136,7 @@
}
.mini-day.has-events::after {
content: '';
content: "";
position: absolute;
bottom: 2px;
width: 4px;
@ -199,7 +200,7 @@
}
.calendar-checkbox.checked::after {
content: '✓';
content: "✓";
display: flex;
align-items: center;
justify-content: center;
@ -562,11 +563,26 @@
cursor: pointer;
}
.month-event.blue { background: rgba(59, 130, 246, 0.2); color: #3b82f6; }
.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; }
.month-event.blue {
background: rgba(59, 130, 246, 0.2);
color: #3b82f6;
}
.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 {
font-size: 11px;
@ -592,11 +608,26 @@
z-index: 5;
}
.calendar-event.blue { background: rgba(59, 130, 246, 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; }
.calendar-event.blue {
background: rgba(59, 130, 246, 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 {
font-weight: 500;
@ -622,7 +653,7 @@
}
.current-time-indicator::before {
content: '';
content: "";
position: absolute;
left: -5px;
top: -4px;
@ -782,7 +813,9 @@
.color-option input:checked + .color-dot {
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 {
@ -909,7 +942,9 @@
text-decoration: none;
border-radius: 8px;
cursor: pointer;
transition: background 0.2s, border-color 0.2s;
transition:
background 0.2s,
border-color 0.2s;
}
.ical-btn:hover {

View file

@ -14,7 +14,7 @@
<!-- Quick Actions -->
<div class="sidebar-actions">
<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-swap="innerHTML">
<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">
<h3>My Calendars</h3>
<button class="btn-icon-sm" title="Add Calendar"
hx-get="/api/calendar/new"
hx-get="/ui/calendar/new"
hx-target="#calendar-modal-content">
<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>
@ -64,7 +64,7 @@
</button>
</div>
<div class="calendars-list" id="calendars-list"
hx-get="/api/calendar/list"
hx-get="/ui/calendar/list"
hx-trigger="load"
hx-swap="innerHTML">
<!-- Calendars loaded here -->
@ -75,7 +75,7 @@
<div class="sidebar-section">
<h3>Upcoming</h3>
<div class="upcoming-events" id="upcoming-events"
hx-get="/api/calendar/upcoming"
hx-get="/ui/calendar/upcoming"
hx-trigger="load"
hx-swap="innerHTML">
<!-- Upcoming events loaded here -->
@ -423,7 +423,7 @@
<div class="form-group">
<label>Import to calendar</label>
<select name="calendar_id"
hx-get="/api/calendar/list"
hx-get="/ui/calendar/list"
hx-trigger="load"
hx-swap="innerHTML">
<option value="">Default Calendar</option>
@ -1692,6 +1692,8 @@
function updateCurrentTimeIndicator() {
const indicator = document.getElementById('current-time-indicator');
if (!indicator) return; // Guard against null element
const now = new Date();
const minutes = now.getHours() * 60 + now.getMinutes();
const top = (minutes / 60) * 48; // 48px per hour

View file

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

File diff suppressed because it is too large Load diff

View file

@ -83,6 +83,22 @@
--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 */
/* ============================================ */
@ -94,7 +110,9 @@
}
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);
color: var(--text);
min-height: 100vh;
@ -167,7 +185,9 @@ body {
color: var(--text-secondary);
border-radius: 8px;
cursor: pointer;
transition: background 0.2s, color 0.2s;
transition:
background 0.2s,
color 0.2s;
display: flex;
align-items: 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 {
display: grid;
grid-template-columns: repeat(3, 1fr);
@ -385,35 +447,139 @@ body {
}
/* Theme Preview Colors */
.theme-dark .theme-option-inner { background: #0f172a; color: #f8fafc; }
.theme-dark .theme-option-header { background: #1e293b; }
.theme-dark .theme-option-dot { background: #3b82f6; }
.theme-dark .theme-option-line { background: #334155; }
.theme-dark .theme-option-inner {
background: #0f172a;
color: #f8fafc;
}
.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-header { background: #ffffff; }
.theme-light .theme-option-dot { background: #3b82f6; }
.theme-light .theme-option-line { background: #e2e8f0; }
.theme-light .theme-option-inner {
background: #f8fafc;
color: #1e293b;
}
.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-header { background: #1a2f47; }
.theme-blue .theme-option-dot { background: #0ea5e9; }
.theme-blue .theme-option-line { background: #2d4a6f; }
.theme-blue .theme-option-inner {
background: #0c1929;
color: #f8fafc;
}
.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-header { background: #2d1b4e; }
.theme-purple .theme-option-dot { background: #a855f7; }
.theme-purple .theme-option-line { background: #4c2f7e; }
.theme-purple .theme-option-inner {
background: #1a0a2e;
color: #f8fafc;
}
.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-header { background: #14332a; }
.theme-green .theme-option-dot { background: #22c55e; }
.theme-green .theme-option-line { background: #28604f; }
.theme-green .theme-option-inner {
background: #0a1f15;
color: #f8fafc;
}
.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-header { background: #2d1c0f; }
.theme-orange .theme-option-dot { background: #f97316; }
.theme-orange .theme-option-line { background: #5c3a1e; }
.theme-orange .theme-option-inner {
background: #1a1008;
color: #f8fafc;
}
.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 */
@ -647,7 +813,12 @@ body {
/* ============================================ */
.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%;
animation: skeleton-loading 1.5s infinite;
border-radius: 4px;
@ -668,8 +839,12 @@ body {
}
@keyframes skeleton-loading {
0% { background-position: 200% 0; }
100% { background-position: -200% 0; }
0% {
background-position: 200% 0;
}
100% {
background-position: -200% 0;
}
}
/* ============================================ */
@ -687,7 +862,9 @@ body {
}
@keyframes spin {
to { transform: rotate(360deg); }
to {
transform: rotate(360deg);
}
}
/* ============================================ */
@ -717,10 +894,18 @@ body {
animation: slideIn 0.3s ease;
}
.notification.success { border-left: 3px solid var(--success); }
.notification.error { border-left: 3px solid var(--error); }
.notification.warning { border-left: 3px solid var(--warning); }
.notification.info { border-left: 3px solid var(--info); }
.notification.success {
border-left: 3px solid var(--success);
}
.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 {
from {

View file

@ -413,18 +413,44 @@ select:focus {
/* MODALS */
/* ============================================ */
.modal {
.modal-overlay {
position: fixed;
inset: 0;
background: hsla(var(--foreground) / 0.5);
background: rgba(0, 0, 0, 0.7);
display: flex;
align-items: center;
justify-content: center;
z-index: 200;
z-index: 1100;
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 {
display: none;
}
@ -442,23 +468,12 @@ select:focus {
width: 100%;
max-width: 480px;
max-height: calc(100vh - 32px);
background: hsl(var(--card));
background: var(--surface, #161616);
border: 1px solid var(--border, #2a2a2a);
border-radius: 12px;
overflow: hidden;
display: flex;
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 {
@ -482,28 +497,97 @@ select:focus {
display: flex;
align-items: center;
justify-content: space-between;
padding: 16px 20px;
border-bottom: 1px solid hsl(var(--border));
padding: 20px 24px;
border-bottom: 1px solid var(--border, #2a2a2a);
}
.modal-header h3 {
margin: 0;
font-size: 18px;
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 {
flex: 1;
padding: 20px;
overflow-y: auto;
padding: 24px;
}
.modal-footer {
display: flex;
justify-content: flex-end;
gap: 12px;
padding: 16px 20px;
border-top: 1px solid hsl(var(--border));
padding: 16px 24px;
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::after {
content: '';
content: "";
flex: 1;
height: 1px;
background: hsl(var(--border));
@ -1019,28 +1103,70 @@ select:focus {
justify-content: space-between;
}
.gap-1 { gap: 4px; }
.gap-2 { gap: 8px; }
.gap-3 { gap: 12px; }
.gap-4 { gap: 16px; }
.gap-5 { gap: 20px; }
.gap-1 {
gap: 4px;
}
.gap-2 {
gap: 8px;
}
.gap-3 {
gap: 12px;
}
.gap-4 {
gap: 16px;
}
.gap-5 {
gap: 20px;
}
.p-1 { padding: 4px; }
.p-2 { padding: 8px; }
.p-3 { padding: 12px; }
.p-4 { padding: 16px; }
.p-5 { padding: 20px; }
.p-1 {
padding: 4px;
}
.p-2 {
padding: 8px;
}
.p-3 {
padding: 12px;
}
.p-4 {
padding: 16px;
}
.p-5 {
padding: 20px;
}
.m-1 { margin: 4px; }
.m-2 { margin: 8px; }
.m-3 { margin: 12px; }
.m-4 { margin: 16px; }
.m-5 { margin: 20px; }
.m-1 {
margin: 4px;
}
.m-2 {
margin: 8px;
}
.m-3 {
margin: 12px;
}
.m-4 {
margin: 16px;
}
.m-5 {
margin: 20px;
}
.rounded { border-radius: 6px; }
.rounded-lg { border-radius: 12px; }
.rounded-full { border-radius: 9999px; }
.rounded {
border-radius: 6px;
}
.rounded-lg {
border-radius: 12px;
}
.rounded-full {
border-radius: 9999px;
}
.shadow-sm { box-shadow: 0 1px 2px hsla(var(--foreground) / 0.05); }
.shadow { box-shadow: 0 2px 8px hsla(var(--foreground) / 0.08); }
.shadow-lg { box-shadow: 0 8px 24px hsla(var(--foreground) / 0.12); }
.shadow-sm {
box-shadow: 0 1px 2px hsla(var(--foreground) / 0.05);
}
.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 {
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;
color: #e2e8f0;
height: 100vh;
@ -49,8 +55,9 @@ nav a.active {
/* Main Content */
#main-content {
height: calc(100vh - 60px);
height: calc(100vh - 64px);
overflow: hidden;
position: relative;
}
.content-section {
@ -81,7 +88,9 @@ button:disabled {
}
/* Utility */
h1, h2, h3 {
h1,
h2,
h3 {
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 {
display: flex;
height: calc(100vh - 60px);
background: var(--bg);
height: 100%;
min-height: 0;
background: var(--bg, var(--bg-primary, #0a0a0f));
color: var(--text, var(--text-primary, #ffffff));
}
/* Drive Sidebar */
.drive-sidebar {
width: 240px;
background: var(--surface);
border-right: 1px solid var(--border);
min-width: 240px;
background: var(--surface, var(--bg-secondary, #12121a));
border-right: 1px solid var(--border, #2a2a3a);
display: flex;
flex-direction: column;
flex-shrink: 0;
overflow: hidden;
}
.drive-sidebar-header {
@ -76,6 +81,87 @@
display: flex;
flex-direction: column;
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 {
@ -164,11 +250,26 @@
border-radius: 8px;
}
.file-icon.folder { background: rgba(249, 115, 22, 0.1); color: #f97316; }
.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-icon.folder {
background: rgba(249, 115, 22, 0.1);
color: #f97316;
}
.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 {
font-size: 13px;

View file

@ -170,24 +170,6 @@
</div>
</div>
<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
class="btn-icon view-toggle"
data-view="grid"

View file

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

View file

@ -148,21 +148,7 @@
</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="view-toggle">

View file

@ -8,13 +8,14 @@
name="description"
content="General Bots - AI-powered workspace"
/>
<meta name="theme-color" content="#3b82f6" />
<meta name="theme-color" content="#d4f505" />
<!-- Styles -->
<link rel="stylesheet" href="css/app.css" />
<link rel="stylesheet" href="css/apps-extended.css" />
<link rel="stylesheet" href="css/components.css" />
<link rel="stylesheet" href="css/base.css" />
<link rel="stylesheet" href="css/theme-sentient.css" />
<!-- App-specific CSS -->
<link rel="stylesheet" href="chat/chat.css" />
@ -41,7 +42,7 @@
</script>
</head>
<body>
<body data-theme="sentient">
<!-- Loading overlay -->
<div class="loading-overlay" id="loadingOverlay">
<div class="loading-spinner"></div>
@ -231,7 +232,13 @@
</svg>
<span class="notification-badge">3</span>
</button>
<button class="icon-button" title="Settings">
<button
class="icon-button"
id="settingsBtn"
title="Settings"
aria-expanded="false"
aria-haspopup="true"
>
<svg
width="20"
height="20"
@ -247,6 +254,108 @@
</svg>
</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 -->
<nav
class="apps-dropdown"
@ -804,12 +913,17 @@
// Simple apps menu handling
const appsBtn = document.getElementById("appsButton");
const appsDropdown = document.getElementById("appsDropdown");
const settingsBtn = document.getElementById("settingsBtn");
const settingsPanel = document.getElementById("settingsPanel");
if (appsBtn && appsDropdown) {
appsBtn.addEventListener("click", (e) => {
e.stopPropagation();
const isOpen = appsDropdown.classList.toggle("show");
appsBtn.setAttribute("aria-expanded", isOpen);
// Close settings panel
if (settingsPanel)
settingsPanel.classList.remove("show");
});
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
document.querySelectorAll(".app-item").forEach((item) => {
item.addEventListener("click", function () {

View file

@ -32,11 +32,19 @@ if (settingsBtn) {
// Close dropdowns when clicking outside
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");
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");
settingsBtn.setAttribute("aria-expanded", "false");
}
@ -99,10 +107,31 @@ document.body.addEventListener("htmx:afterSwap", (e) => {
});
// Theme handling
// Available themes: dark, light, blue, purple, green, orange, sentient
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.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) => {
option.addEventListener("click", () => {
@ -111,9 +140,20 @@ themeOptions.forEach((option) => {
localStorage.setItem("gb-theme", theme);
themeOptions.forEach((o) => o.classList.remove("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
function toggleQuickSetting(el) {
el.classList.toggle("active");
@ -135,7 +175,7 @@ function showKeyboardShortcuts() {
window.showNotification(
"Alt+1-9,0 for apps, Alt+A Admin, Alt+M Monitoring, Alt+S Settings, Alt+, quick settings",
"info",
8000
8000,
);
}
@ -184,7 +224,9 @@ document.addEventListener("keydown", function (e) {
if (!appsGrid || !appsGrid.closest(".show")) return;
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;
@ -317,14 +359,17 @@ document.body.addEventListener("htmx:responseError", function (e) {
let currentRetries = htmxRetryConfig.retryCount.get(retryKey) || 0;
// 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);
const delay = htmxRetryConfig.retryDelay * Math.pow(2, currentRetries);
window.showNotification(
`Request failed. Retrying in ${delay / 1000}s... (${currentRetries + 1}/${htmxRetryConfig.maxRetries})`,
"warning",
delay
delay,
);
setTimeout(() => {
@ -342,9 +387,11 @@ document.body.addEventListener("htmx:responseError", function (e) {
} else if (xhr.status === 404) {
errorMessage = "The requested content was not found.";
} 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) {
errorMessage = "Unable to connect. Please check your internet connection.";
errorMessage =
"Unable to connect. Please check your internet connection.";
}
if (target && target.id === "main-content") {
@ -365,10 +412,18 @@ document.body.addEventListener("htmx:afterRequest", function (e) {
// Handle timeout errors
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)
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
(function () {
'use strict';
"use strict";
// Configuration
const config = {
wsUrl: '/ws',
apiBase: '/api',
wsUrl: "/ws",
apiBase: "/api",
reconnectDelay: 3000,
maxReconnectAttempts: 5
maxReconnectAttempts: 5,
};
// State
@ -17,50 +17,70 @@
// Initialize HTMX extensions
function initHTMX() {
// Configure HTMX
htmx.config.defaultSwapStyle = 'innerHTML';
htmx.config.defaultSwapStyle = "innerHTML";
htmx.config.defaultSettleDelay = 100;
htmx.config.timeout = 10000;
// Add CSRF token to all requests if available
document.body.addEventListener('htmx:configRequest', (event) => {
const token = localStorage.getItem('csrf_token');
document.body.addEventListener("htmx:configRequest", (event) => {
const token = localStorage.getItem("csrf_token");
if (token) {
event.detail.headers['X-CSRF-Token'] = token;
event.detail.headers["X-CSRF-Token"] = token;
}
});
// Handle errors globally
document.body.addEventListener('htmx:responseError', (event) => {
console.error('HTMX Error:', event.detail);
showNotification('Connection error. Please try again.', 'error');
document.body.addEventListener("htmx:responseError", (event) => {
console.error("HTMX Error:", event.detail);
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
document.body.addEventListener('htmx:afterSwap', (event) => {
document.body.addEventListener("htmx:afterSwap", (event) => {
// Auto-scroll messages if in chat
const messages = document.getElementById('messages');
const messages = document.getElementById("messages");
if (messages && event.detail.target === messages) {
messages.scrollTop = messages.scrollHeight;
}
});
// Handle WebSocket messages
document.body.addEventListener('htmx:wsMessage', (event) => {
document.body.addEventListener("htmx:wsMessage", (event) => {
handleWebSocketMessage(JSON.parse(event.detail.message));
});
// Handle WebSocket connection events
document.body.addEventListener('htmx:wsConnecting', () => {
updateConnectionStatus('connecting');
document.body.addEventListener("htmx:wsConnecting", () => {
updateConnectionStatus("connecting");
});
document.body.addEventListener('htmx:wsOpen', () => {
updateConnectionStatus('connected');
document.body.addEventListener("htmx:wsOpen", () => {
updateConnectionStatus("connected");
reconnectAttempts = 0;
});
document.body.addEventListener('htmx:wsClose', () => {
updateConnectionStatus('disconnected');
document.body.addEventListener("htmx:wsClose", () => {
updateConnectionStatus("disconnected");
attemptReconnect();
});
}
@ -68,30 +88,30 @@
// Handle WebSocket messages
function handleWebSocketMessage(message) {
switch (message.type) {
case 'message':
case "message":
appendMessage(message);
break;
case 'notification':
case "notification":
showNotification(message.text, message.severity);
break;
case 'status':
case "status":
updateStatus(message);
break;
case 'suggestion':
case "suggestion":
addSuggestion(message.text);
break;
default:
console.log('Unknown message type:', message.type);
console.log("Unknown message type:", message.type);
}
}
// Append message to chat
function appendMessage(message) {
const messagesEl = document.getElementById('messages');
const messagesEl = document.getElementById("messages");
if (!messagesEl) return;
const messageEl = document.createElement('div');
messageEl.className = `message ${message.sender === 'user' ? 'user' : 'bot'}`;
const messageEl = document.createElement("div");
messageEl.className = `message ${message.sender === "user" ? "user" : "bot"}`;
messageEl.innerHTML = `
<div class="message-content">
<span class="sender">${message.sender}</span>
@ -106,16 +126,16 @@
// Add suggestion chip
function addSuggestion(text) {
const suggestionsEl = document.getElementById('suggestions');
const suggestionsEl = document.getElementById("suggestions");
if (!suggestionsEl) return;
const chip = document.createElement('button');
chip.className = 'suggestion-chip';
const chip = document.createElement("button");
chip.className = "suggestion-chip";
chip.textContent = text;
chip.setAttribute('hx-post', '/api/sessions/current/message');
chip.setAttribute('hx-vals', JSON.stringify({content: text}));
chip.setAttribute('hx-target', '#messages');
chip.setAttribute('hx-swap', 'beforeend');
chip.setAttribute("hx-post", "/api/sessions/current/message");
chip.setAttribute("hx-vals", JSON.stringify({ content: text }));
chip.setAttribute("hx-target", "#messages");
chip.setAttribute("hx-swap", "beforeend");
suggestionsEl.appendChild(chip);
htmx.process(chip);
@ -123,7 +143,7 @@
// Update connection status
function updateConnectionStatus(status) {
const statusEl = document.getElementById('connectionStatus');
const statusEl = document.getElementById("connectionStatus");
if (!statusEl) return;
statusEl.className = `connection-status ${status}`;
@ -132,7 +152,7 @@
// Update general status
function updateStatus(message) {
const statusEl = document.getElementById('status-' + message.id);
const statusEl = document.getElementById("status-" + message.id);
if (statusEl) {
statusEl.textContent = message.text;
statusEl.className = `status ${message.severity}`;
@ -140,16 +160,16 @@
}
// Show notification
function showNotification(text, type = 'info') {
const notification = document.createElement('div');
function showNotification(text, type = "info") {
const notification = document.createElement("div");
notification.className = `notification ${type}`;
notification.textContent = text;
const container = document.getElementById('notifications') || document.body;
const container = document.getElementById("notifications") || document.body;
container.appendChild(notification);
setTimeout(() => {
notification.classList.add('fade-out');
notification.classList.add("fade-out");
setTimeout(() => notification.remove(), 300);
}, 3000);
}
@ -157,86 +177,86 @@
// Attempt to reconnect WebSocket
function attemptReconnect() {
if (reconnectAttempts >= config.maxReconnectAttempts) {
showNotification('Connection lost. Please refresh the page.', 'error');
showNotification("Connection lost. Please refresh the page.", "error");
return;
}
reconnectAttempts++;
setTimeout(() => {
console.log(`Reconnection attempt ${reconnectAttempts}...`);
htmx.trigger(document.body, 'htmx:wsReconnect');
htmx.trigger(document.body, "htmx:wsReconnect");
}, config.reconnectDelay);
}
// Utility: Escape HTML
function escapeHtml(text) {
const div = document.createElement('div');
const div = document.createElement("div");
div.textContent = text;
return div.innerHTML;
}
// Utility: Format timestamp
function formatTime(timestamp) {
if (!timestamp) return '';
if (!timestamp) return "";
const date = new Date(timestamp);
return date.toLocaleTimeString('en-US', {
hour: 'numeric',
minute: '2-digit',
hour12: true
return date.toLocaleTimeString("en-US", {
hour: "numeric",
minute: "2-digit",
hour12: true,
});
}
// Handle navigation
function initNavigation() {
// Update active nav item on page change
document.addEventListener('htmx:pushedIntoHistory', (event) => {
document.addEventListener("htmx:pushedIntoHistory", (event) => {
const path = event.detail.path;
updateActiveNav(path);
});
// Handle browser back/forward
window.addEventListener('popstate', (event) => {
window.addEventListener("popstate", (event) => {
updateActiveNav(window.location.pathname);
});
}
// Update active navigation item
function updateActiveNav(path) {
document.querySelectorAll('.nav-item, .app-item').forEach(item => {
const href = item.getAttribute('href');
if (href === path || (path === '/' && href === '/chat')) {
item.classList.add('active');
document.querySelectorAll(".nav-item, .app-item").forEach((item) => {
const href = item.getAttribute("href");
if (href === path || (path === "/" && href === "/chat")) {
item.classList.add("active");
} else {
item.classList.remove('active');
item.classList.remove("active");
}
});
}
// Initialize keyboard shortcuts
function initKeyboardShortcuts() {
document.addEventListener('keydown', (e) => {
document.addEventListener("keydown", (e) => {
// Send message on Enter (when in input)
if (e.key === 'Enter' && !e.shiftKey) {
const input = document.getElementById('messageInput');
if (e.key === "Enter" && !e.shiftKey) {
const input = document.getElementById("messageInput");
if (input && document.activeElement === input) {
e.preventDefault();
const form = input.closest('form');
const form = input.closest("form");
if (form) {
htmx.trigger(form, 'submit');
htmx.trigger(form, "submit");
}
}
}
// Focus input on /
if (e.key === '/' && document.activeElement.tagName !== 'INPUT') {
if (e.key === "/" && document.activeElement.tagName !== "INPUT") {
e.preventDefault();
const input = document.getElementById('messageInput');
const input = document.getElementById("messageInput");
if (input) input.focus();
}
// Escape to blur input
if (e.key === 'Escape') {
const input = document.getElementById('messageInput');
if (e.key === "Escape") {
const input = document.getElementById("messageInput");
if (input && document.activeElement === input) {
input.blur();
}
@ -246,21 +266,23 @@
// Initialize scroll behavior
function initScrollBehavior() {
const scrollBtn = document.getElementById('scrollToBottom');
const messages = document.getElementById('messages');
const scrollBtn = document.getElementById("scrollToBottom");
const messages = document.getElementById("messages");
if (scrollBtn && messages) {
// Show/hide scroll button
messages.addEventListener('scroll', () => {
const isAtBottom = messages.scrollHeight - messages.scrollTop <= messages.clientHeight + 100;
scrollBtn.style.display = isAtBottom ? 'none' : 'flex';
messages.addEventListener("scroll", () => {
const isAtBottom =
messages.scrollHeight - messages.scrollTop <=
messages.clientHeight + 100;
scrollBtn.style.display = isAtBottom ? "none" : "flex";
});
// Scroll to bottom on click
scrollBtn.addEventListener('click', () => {
scrollBtn.addEventListener("click", () => {
messages.scrollTo({
top: messages.scrollHeight,
behavior: 'smooth'
behavior: "smooth",
});
});
}
@ -275,7 +297,7 @@
// Main initialization
function init() {
console.log('Initializing HTMX application...');
console.log("Initializing HTMX application...");
// Initialize HTMX
initHTMX();
@ -295,12 +317,12 @@
// Set initial active nav
updateActiveNav(window.location.pathname);
console.log('HTMX application initialized');
console.log("HTMX application initialized");
}
// Wait for DOM and HTMX to be ready
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", init);
} else {
init();
}
@ -310,6 +332,6 @@
showNotification,
appendMessage,
updateConnectionStatus,
config
config,
};
})();

View file

@ -12,7 +12,7 @@
<!-- Folder List -->
<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">
<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"/>
@ -20,20 +20,20 @@
<span>Inbox</span>
<span class="folder-badge unread" id="inbox-count">0</span>
</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">
<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>
<span>Starred</span>
</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">
<line x1="22" y1="2" x2="11" y2="13"/>
<polygon points="22 2 15 22 11 13 2 9 22 2"/>
</svg>
<span>Sent</span>
</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">
<circle cx="12" cy="12" r="10"/>
<polyline points="12 6 12 12 16 14"/>
@ -41,7 +41,7 @@
<span>Scheduled</span>
<span class="folder-badge" id="scheduled-count">0</span>
</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">
<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"/>
@ -58,7 +58,7 @@
</svg>
<span>Tracking</span>
</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">
<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"/>
@ -66,7 +66,7 @@
</svg>
<span>Spam</span>
</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">
<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"/>
@ -87,7 +87,7 @@
</button>
</div>
<div id="accounts-list"
hx-get="/api/email/accounts"
hx-get="/ui/email/accounts"
hx-trigger="load"
hx-swap="innerHTML">
<!-- Accounts loaded here -->
@ -105,7 +105,7 @@
</svg>
</button>
</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;">
<span class="label-dot"></span>
<span>Important</span>
@ -169,7 +169,7 @@
<line x1="21" y1="21" x2="16.65" y2="16.65"/>
</svg>
<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"/>
</div>
<button class="icon-btn" onclick="refreshMailList()" title="Refresh">
@ -212,7 +212,7 @@
</button>
</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="spinner"></div>
<p>Loading emails...</p>
@ -421,7 +421,7 @@
</svg>
</button>
</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>
<div class="modal-footer">
@ -443,7 +443,7 @@
</svg>
</button>
</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>
<div class="modal-footer">
@ -465,7 +465,7 @@
</svg>
</button>
</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>
<div class="modal-footer">
@ -487,7 +487,7 @@
</svg>
</button>
</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">
<label class="toggle-label">
<input type="checkbox" name="enabled" id="autoresponder-enabled"/>
@ -1013,19 +1013,28 @@
to { transform: rotate(360deg); }
}
/* Modals */
.modal {
/* Modals - dialog elements are hidden by default */
dialog.modal {
position: fixed;
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;
align-items: center;
justify-content: center;
z-index: 1000;
padding: 1rem;
}
.modal::backdrop {
dialog.modal::backdrop {
background: rgba(0, 0, 0, 0.5);
}

View file

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

View file

@ -1,67 +1,75 @@
/* Monitoring module - shared/base JavaScript */
function setActiveNav(element) {
document.querySelectorAll('.monitoring-nav .nav-item').forEach(item => {
item.classList.remove('active');
document.querySelectorAll(".monitoring-nav .nav-item").forEach((item) => {
item.classList.remove("active");
});
element.classList.add('active');
element.classList.add("active");
// Update page title
const title = element.querySelector('span:not(.alert-badge):not(.health-indicator)').textContent;
document.getElementById('page-title').textContent = title;
const title = element.querySelector(
"span:not(.alert-badge):not(.health-indicator)",
).textContent;
document.getElementById("page-title").textContent = title;
}
function updateTimeRange(range) {
// Store selected time range
localStorage.setItem('monitoring-time-range', range);
localStorage.setItem("monitoring-time-range", range);
// Trigger refresh of current view
htmx.trigger('#monitoring-content', 'refresh');
htmx.trigger("#monitoring-content", "refresh");
}
function refreshMonitoring() {
htmx.trigger('#monitoring-content', 'refresh');
htmx.trigger("#monitoring-content", "refresh");
// Visual feedback
const btn = event.currentTarget;
btn.classList.add('active');
setTimeout(() => btn.classList.remove('active'), 500);
btn.classList.add("active");
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() {
autoRefresh = !autoRefresh;
const btn = document.getElementById('auto-refresh-btn');
btn.classList.toggle('active', autoRefresh);
const btn = document.getElementById("auto-refresh-btn");
btn.classList.toggle("active", autoRefresh);
if (autoRefresh) {
// Re-enable polling by refreshing the page content
htmx.trigger('#monitoring-content', 'refresh');
htmx.trigger("#monitoring-content", "refresh");
}
}
function exportData() {
const timeRange = document.getElementById('time-range').value;
window.open(`/api/monitoring/export?range=${timeRange}`, '_blank');
const timeRange = document.getElementById("time-range").value;
window.open(`/api/monitoring/export?range=${timeRange}`, "_blank");
}
// Initialize
document.addEventListener('DOMContentLoaded', function() {
document.addEventListener("DOMContentLoaded", function () {
// Restore time range preference
const savedRange = localStorage.getItem('monitoring-time-range');
const savedRange = localStorage.getItem("monitoring-time-range");
if (savedRange) {
const timeRangeEl = document.getElementById('time-range');
const timeRangeEl = document.getElementById("time-range");
if (timeRangeEl) timeRangeEl.value = savedRange;
}
// Set auto-refresh button state
const autoRefreshBtn = document.getElementById('auto-refresh-btn');
if (autoRefreshBtn) autoRefreshBtn.classList.toggle('active', autoRefresh);
const autoRefreshBtn = document.getElementById("auto-refresh-btn");
if (autoRefreshBtn) autoRefreshBtn.classList.toggle("active", autoRefresh);
});
// Handle HTMX events for loading states
document.body.addEventListener('htmx:beforeRequest', function(evt) {
if (evt.target.id === 'monitoring-content') {
evt.target.innerHTML = '<div class="loading-state"><div class="spinner"></div><p>Loading...</p></div>';
document.body.addEventListener("htmx:beforeRequest", function (evt) {
if (evt.target.id === "monitoring-content") {
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 {
display: flex;
height: calc(100vh - 60px);
background: var(--background);
color: var(--foreground);
height: 100%;
min-height: 0;
background: var(--background, var(--bg-primary, #0a0a0f));
color: var(--foreground, var(--text-primary, #ffffff));
}
/* Sidebar */
@ -398,12 +399,22 @@
animation: bounce 1.4s infinite ease-in-out both;
}
.indicator-dots span:nth-child(1) { animation-delay: -0.32s; }
.indicator-dots span:nth-child(2) { animation-delay: -0.16s; }
.indicator-dots span:nth-child(1) {
animation-delay: -0.32s;
}
.indicator-dots span:nth-child(2) {
animation-delay: -0.16s;
}
@keyframes bounce {
0%, 80%, 100% { transform: scale(0); }
40% { transform: scale(1); }
0%,
80%,
100% {
transform: scale(0);
}
40% {
transform: scale(1);
}
}
.indicator-text {
@ -557,7 +568,7 @@
background: var(--muted);
padding: 2px 6px;
border-radius: 4px;
font-family: 'JetBrains Mono', 'Fira Code', monospace;
font-family: "JetBrains Mono", "Fira Code", monospace;
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 {
display: flex;
flex-direction: column;
min-height: 100vh;
background: var(--sentient-bg-primary);
color: var(--sentient-text-primary);
font-family: var(--sentient-font-family);
height: 100%;
min-height: 0;
background: var(--bg-primary, var(--sentient-bg-primary, #0a0a0f));
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;
justify-content: space-between;
padding: 16px 24px;
background: var(--sentient-bg-secondary);
border-bottom: 1px solid var(--sentient-border);
background: var(--bg-secondary, var(--sentient-bg-secondary, #12121a));
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 {
@ -44,6 +75,18 @@
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 {
width: 32px;
height: 32px;
@ -211,6 +254,7 @@
display: flex;
flex: 1;
overflow: hidden;
min-height: 0;
}
/* =============================================================================
@ -221,13 +265,17 @@
flex: 1;
display: flex;
flex-direction: column;
border-right: 1px solid var(--sentient-border);
border-right: 1px solid var(--border, var(--sentient-border, #2a2a3a));
overflow: hidden;
min-height: 0;
}
.tasks-list-header {
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 {
@ -240,13 +288,13 @@
.tasks-list-title h1 {
font-size: 24px;
font-weight: 700;
color: var(--sentient-text-primary);
color: var(--text-primary, var(--sentient-text-primary, #ffffff));
margin: 0;
}
.tasks-count {
font-size: 14px;
color: var(--sentient-text-muted);
color: var(--text-muted, var(--sentient-text-muted, #6b6b80));
}
/* Status Filter Pills */
@ -254,6 +302,8 @@
display: flex;
gap: 8px;
flex-wrap: wrap;
position: relative;
z-index: 1;
}
.status-pill {

View file

@ -14,6 +14,24 @@
<span class="tasks-count" id="tasks-total-count"
>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>
<!-- Status Filter Pills -->
@ -505,4 +523,156 @@
</main>
</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
// =============================================================================
const TasksState = {
// Prevent duplicate declaration when script is reloaded via HTMX
if (typeof TasksState === "undefined") {
var TasksState = {
selectedTaskId: 2, // Default selected task
currentFilter: "complete",
tasks: [],
wsConnection: null,
agentLogPaused: false,
};
}
// =============================================================================
// INITIALIZATION