Update UI components, styling, and add theme-sentient and intents
This commit is contained in:
parent
db06e42289
commit
9fe234aa3c
28 changed files with 5140 additions and 1791 deletions
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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 */
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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 %}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,102 +1,111 @@
|
|||
* { 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;
|
||||
background: #0f172a;
|
||||
color: #e2e8f0;
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
font-family:
|
||||
-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu,
|
||||
sans-serif;
|
||||
background: #0f172a;
|
||||
color: #e2e8f0;
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Navbar */
|
||||
nav {
|
||||
background: #1e293b;
|
||||
border-bottom: 2px solid #334155;
|
||||
padding: 0 1rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 60px;
|
||||
gap: 0.5rem;
|
||||
background: #1e293b;
|
||||
border-bottom: 2px solid #334155;
|
||||
padding: 0 1rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 60px;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
nav .logo {
|
||||
font-size: 1.5rem;
|
||||
font-weight: bold;
|
||||
background: linear-gradient(135deg, #3b82f6, #8b5cf6);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
margin-right: auto;
|
||||
font-size: 1.5rem;
|
||||
font-weight: bold;
|
||||
background: linear-gradient(135deg, #3b82f6, #8b5cf6);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
nav a {
|
||||
color: #94a3b8;
|
||||
text-decoration: none;
|
||||
padding: 0.75rem 1.25rem;
|
||||
border-radius: 0.5rem;
|
||||
transition: all 0.2s;
|
||||
font-weight: 500;
|
||||
color: #94a3b8;
|
||||
text-decoration: none;
|
||||
padding: 0.75rem 1.25rem;
|
||||
border-radius: 0.5rem;
|
||||
transition: all 0.2s;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
nav a:hover {
|
||||
background: #334155;
|
||||
color: #e2e8f0;
|
||||
background: #334155;
|
||||
color: #e2e8f0;
|
||||
}
|
||||
|
||||
nav a.active {
|
||||
background: #3b82f6;
|
||||
color: white;
|
||||
background: #3b82f6;
|
||||
color: white;
|
||||
}
|
||||
|
||||
/* Main Content */
|
||||
#main-content {
|
||||
height: calc(100vh - 60px);
|
||||
overflow: hidden;
|
||||
height: calc(100vh - 64px);
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.content-section {
|
||||
display: none;
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
display: none;
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.content-section.active {
|
||||
display: block;
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* Panel Styles */
|
||||
.panel {
|
||||
background: #1e293b;
|
||||
border: 1px solid #334155;
|
||||
border-radius: 0.5rem;
|
||||
background: #1e293b;
|
||||
border: 1px solid #334155;
|
||||
border-radius: 0.5rem;
|
||||
}
|
||||
|
||||
/* Buttons */
|
||||
button {
|
||||
font-family: inherit;
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
button:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
/* Utility */
|
||||
h1, h2, h3 {
|
||||
margin-bottom: 1rem;
|
||||
h1,
|
||||
h2,
|
||||
h3 {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.text-sm {
|
||||
font-size: 0.875rem;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.text-xs {
|
||||
font-size: 0.75rem;
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.text-gray {
|
||||
color: #94a3b8;
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
[x-cloak] {
|
||||
display: none !important;
|
||||
display: none !important;
|
||||
}
|
||||
|
|
|
|||
977
ui/suite/css/theme-sentient.css
Normal file
977
ui/suite/css/theme-sentient.css
Normal 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 {
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -1,162 +1,199 @@
|
|||
/* Drive Module JavaScript */
|
||||
|
||||
(function() {
|
||||
'use strict';
|
||||
(function () {
|
||||
"use strict";
|
||||
|
||||
let selectedFiles = [];
|
||||
let currentPath = '/';
|
||||
let viewMode = 'grid';
|
||||
let selectedFiles = [];
|
||||
let currentPath = "/";
|
||||
let viewMode = "grid";
|
||||
|
||||
function init() {
|
||||
bindNavigation();
|
||||
bindFileSelection();
|
||||
bindDragAndDrop();
|
||||
bindContextMenu();
|
||||
bindKeyboardShortcuts();
|
||||
// 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"));
|
||||
}
|
||||
|
||||
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');
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function bindFileSelection() {
|
||||
document.querySelectorAll('.file-card, .file-list-item').forEach(file => {
|
||||
file.addEventListener('click', function(e) {
|
||||
if (e.ctrlKey || e.metaKey) {
|
||||
this.classList.toggle('selected');
|
||||
} else {
|
||||
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';
|
||||
if (isFolder) {
|
||||
navigateToFolder(this.dataset.path);
|
||||
} else {
|
||||
openFile(this.dataset.id);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function bindDragAndDrop() {
|
||||
const uploadZone = document.querySelector('.upload-zone');
|
||||
if (!uploadZone) return;
|
||||
|
||||
['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'));
|
||||
});
|
||||
|
||||
['dragleave', 'drop'].forEach(event => {
|
||||
uploadZone.addEventListener(event, () => uploadZone.classList.remove('dragover'));
|
||||
});
|
||||
|
||||
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');
|
||||
if (fileCard) {
|
||||
e.preventDefault();
|
||||
showContextMenu(e.clientX, e.clientY, fileCard);
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener('click', hideContextMenu);
|
||||
}
|
||||
|
||||
function bindKeyboardShortcuts() {
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Delete' && selectedFiles.length > 0) {
|
||||
deleteSelectedFiles();
|
||||
}
|
||||
if (e.key === 'a' && (e.ctrlKey || e.metaKey)) {
|
||||
e.preventDefault();
|
||||
selectAllFiles();
|
||||
}
|
||||
if (e.key === 'Escape') {
|
||||
deselectAllFiles();
|
||||
hideContextMenu();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function updateSelectedFiles() {
|
||||
selectedFiles = Array.from(document.querySelectorAll('.file-card.selected, .file-list-item.selected'))
|
||||
.map(f => f.dataset.id);
|
||||
}
|
||||
|
||||
function navigateToFolder(path) {
|
||||
currentPath = path;
|
||||
// HTMX handles actual navigation
|
||||
}
|
||||
|
||||
function openFile(fileId) {
|
||||
console.log('Opening file:', fileId);
|
||||
}
|
||||
|
||||
function handleFileUpload(files) {
|
||||
console.log('Uploading files:', files);
|
||||
}
|
||||
|
||||
function showContextMenu(x, y, fileCard) {
|
||||
hideContextMenu();
|
||||
const menu = document.getElementById('context-menu');
|
||||
if (menu) {
|
||||
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');
|
||||
}
|
||||
|
||||
function selectAllFiles() {
|
||||
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'));
|
||||
selectedFiles = [];
|
||||
}
|
||||
|
||||
function deleteSelectedFiles() {
|
||||
if (confirm(`Delete ${selectedFiles.length} file(s)?`)) {
|
||||
console.log('Deleting:', selectedFiles);
|
||||
}
|
||||
}
|
||||
|
||||
window.DriveModule = { init };
|
||||
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', init);
|
||||
// Toggle selection on clicked element
|
||||
if (isSelected && (event.ctrlKey || event.metaKey)) {
|
||||
element.classList.remove("selected");
|
||||
} else {
|
||||
init();
|
||||
element.classList.add("selected");
|
||||
}
|
||||
|
||||
updateSelectedFiles();
|
||||
};
|
||||
|
||||
function init() {
|
||||
bindNavigation();
|
||||
bindFileSelection();
|
||||
bindDragAndDrop();
|
||||
bindContextMenu();
|
||||
bindKeyboardShortcuts();
|
||||
}
|
||||
|
||||
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");
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function bindFileSelection() {
|
||||
document.querySelectorAll(".file-card, .file-list-item").forEach((file) => {
|
||||
file.addEventListener("click", function (e) {
|
||||
if (e.ctrlKey || e.metaKey) {
|
||||
this.classList.toggle("selected");
|
||||
} else {
|
||||
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";
|
||||
if (isFolder) {
|
||||
navigateToFolder(this.dataset.path);
|
||||
} else {
|
||||
openFile(this.dataset.id);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function bindDragAndDrop() {
|
||||
const uploadZone = document.querySelector(".upload-zone");
|
||||
if (!uploadZone) return;
|
||||
|
||||
["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"),
|
||||
);
|
||||
});
|
||||
|
||||
["dragleave", "drop"].forEach((event) => {
|
||||
uploadZone.addEventListener(event, () =>
|
||||
uploadZone.classList.remove("dragover"),
|
||||
);
|
||||
});
|
||||
|
||||
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");
|
||||
if (fileCard) {
|
||||
e.preventDefault();
|
||||
showContextMenu(e.clientX, e.clientY, fileCard);
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener("click", hideContextMenu);
|
||||
}
|
||||
|
||||
function bindKeyboardShortcuts() {
|
||||
document.addEventListener("keydown", (e) => {
|
||||
if (e.key === "Delete" && selectedFiles.length > 0) {
|
||||
deleteSelectedFiles();
|
||||
}
|
||||
if (e.key === "a" && (e.ctrlKey || e.metaKey)) {
|
||||
e.preventDefault();
|
||||
selectAllFiles();
|
||||
}
|
||||
if (e.key === "Escape") {
|
||||
deselectAllFiles();
|
||||
hideContextMenu();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function updateSelectedFiles() {
|
||||
selectedFiles = Array.from(
|
||||
document.querySelectorAll(
|
||||
".file-card.selected, .file-list-item.selected",
|
||||
),
|
||||
).map((f) => f.dataset.id);
|
||||
}
|
||||
|
||||
function navigateToFolder(path) {
|
||||
currentPath = path;
|
||||
// HTMX handles actual navigation
|
||||
}
|
||||
|
||||
function openFile(fileId) {
|
||||
console.log("Opening file:", fileId);
|
||||
}
|
||||
|
||||
function handleFileUpload(files) {
|
||||
console.log("Uploading files:", files);
|
||||
}
|
||||
|
||||
function showContextMenu(x, y, fileCard) {
|
||||
hideContextMenu();
|
||||
const menu = document.getElementById("context-menu");
|
||||
if (menu) {
|
||||
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");
|
||||
}
|
||||
|
||||
function selectAllFiles() {
|
||||
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"));
|
||||
selectedFiles = [];
|
||||
}
|
||||
|
||||
function deleteSelectedFiles() {
|
||||
if (confirm(`Delete ${selectedFiles.length} file(s)?`)) {
|
||||
console.log("Deleting:", selectedFiles);
|
||||
}
|
||||
}
|
||||
|
||||
window.DriveModule = { init };
|
||||
|
||||
if (document.readyState === "loading") {
|
||||
document.addEventListener("DOMContentLoaded", init);
|
||||
} else {
|
||||
init();
|
||||
}
|
||||
})();
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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 () {
|
||||
|
|
|
|||
|
|
@ -12,253 +12,295 @@ const settingsPanel = document.getElementById("settings-panel");
|
|||
|
||||
// Apps Menu Toggle
|
||||
if (appsBtn) {
|
||||
appsBtn.addEventListener("click", (e) => {
|
||||
e.stopPropagation();
|
||||
const isOpen = appsDropdown.classList.toggle("show");
|
||||
appsBtn.setAttribute("aria-expanded", isOpen);
|
||||
settingsPanel.classList.remove("show");
|
||||
});
|
||||
appsBtn.addEventListener("click", (e) => {
|
||||
e.stopPropagation();
|
||||
const isOpen = appsDropdown.classList.toggle("show");
|
||||
appsBtn.setAttribute("aria-expanded", isOpen);
|
||||
settingsPanel.classList.remove("show");
|
||||
});
|
||||
}
|
||||
|
||||
// Settings Panel Toggle
|
||||
if (settingsBtn) {
|
||||
settingsBtn.addEventListener("click", (e) => {
|
||||
e.stopPropagation();
|
||||
const isOpen = settingsPanel.classList.toggle("show");
|
||||
settingsBtn.setAttribute("aria-expanded", isOpen);
|
||||
appsDropdown.classList.remove("show");
|
||||
});
|
||||
settingsBtn.addEventListener("click", (e) => {
|
||||
e.stopPropagation();
|
||||
const isOpen = settingsPanel.classList.toggle("show");
|
||||
settingsBtn.setAttribute("aria-expanded", isOpen);
|
||||
appsDropdown.classList.remove("show");
|
||||
});
|
||||
}
|
||||
|
||||
// Close dropdowns when clicking outside
|
||||
document.addEventListener("click", (e) => {
|
||||
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)) {
|
||||
settingsPanel.classList.remove("show");
|
||||
settingsBtn.setAttribute("aria-expanded", "false");
|
||||
}
|
||||
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)
|
||||
) {
|
||||
settingsPanel.classList.remove("show");
|
||||
settingsBtn.setAttribute("aria-expanded", "false");
|
||||
}
|
||||
});
|
||||
|
||||
// Escape key closes dropdowns
|
||||
document.addEventListener("keydown", (e) => {
|
||||
if (e.key === "Escape") {
|
||||
if (appsDropdown) appsDropdown.classList.remove("show");
|
||||
if (settingsPanel) settingsPanel.classList.remove("show");
|
||||
if (appsBtn) appsBtn.setAttribute("aria-expanded", "false");
|
||||
if (settingsBtn) settingsBtn.setAttribute("aria-expanded", "false");
|
||||
}
|
||||
if (e.key === "Escape") {
|
||||
if (appsDropdown) appsDropdown.classList.remove("show");
|
||||
if (settingsPanel) settingsPanel.classList.remove("show");
|
||||
if (appsBtn) appsBtn.setAttribute("aria-expanded", "false");
|
||||
if (settingsBtn) settingsBtn.setAttribute("aria-expanded", "false");
|
||||
}
|
||||
});
|
||||
|
||||
// Alt+key shortcuts for navigation
|
||||
document.addEventListener("keydown", (e) => {
|
||||
if (e.altKey && !e.ctrlKey && !e.shiftKey) {
|
||||
const shortcuts = {
|
||||
1: "chat",
|
||||
2: "drive",
|
||||
3: "tasks",
|
||||
4: "mail",
|
||||
5: "calendar",
|
||||
6: "meet",
|
||||
7: "paper",
|
||||
8: "research",
|
||||
9: "sources",
|
||||
0: "analytics",
|
||||
a: "admin",
|
||||
m: "monitoring",
|
||||
};
|
||||
if (shortcuts[e.key]) {
|
||||
e.preventDefault();
|
||||
const link = document.querySelector(`a[href="#${shortcuts[e.key]}"]`);
|
||||
if (link) link.click();
|
||||
if (appsDropdown) appsDropdown.classList.remove("show");
|
||||
}
|
||||
if (e.key === ",") {
|
||||
e.preventDefault();
|
||||
if (settingsPanel) settingsPanel.classList.toggle("show");
|
||||
}
|
||||
if (e.key === "s") {
|
||||
e.preventDefault();
|
||||
const settingsLink = document.querySelector(`a[href="#settings"]`);
|
||||
if (settingsLink) settingsLink.click();
|
||||
}
|
||||
if (e.altKey && !e.ctrlKey && !e.shiftKey) {
|
||||
const shortcuts = {
|
||||
1: "chat",
|
||||
2: "drive",
|
||||
3: "tasks",
|
||||
4: "mail",
|
||||
5: "calendar",
|
||||
6: "meet",
|
||||
7: "paper",
|
||||
8: "research",
|
||||
9: "sources",
|
||||
0: "analytics",
|
||||
a: "admin",
|
||||
m: "monitoring",
|
||||
};
|
||||
if (shortcuts[e.key]) {
|
||||
e.preventDefault();
|
||||
const link = document.querySelector(`a[href="#${shortcuts[e.key]}"]`);
|
||||
if (link) link.click();
|
||||
if (appsDropdown) appsDropdown.classList.remove("show");
|
||||
}
|
||||
if (e.key === ",") {
|
||||
e.preventDefault();
|
||||
if (settingsPanel) settingsPanel.classList.toggle("show");
|
||||
}
|
||||
if (e.key === "s") {
|
||||
e.preventDefault();
|
||||
const settingsLink = document.querySelector(`a[href="#settings"]`);
|
||||
if (settingsLink) settingsLink.click();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Update active app on HTMX swap
|
||||
document.body.addEventListener("htmx:afterSwap", (e) => {
|
||||
if (e.detail.target.id === "main-content") {
|
||||
const hash = window.location.hash || "#chat";
|
||||
document.querySelectorAll(".app-item").forEach((item) => {
|
||||
item.classList.toggle("active", item.getAttribute("href") === hash);
|
||||
});
|
||||
if (settingsPanel) settingsPanel.classList.remove("show");
|
||||
}
|
||||
if (e.detail.target.id === "main-content") {
|
||||
const hash = window.location.hash || "#chat";
|
||||
document.querySelectorAll(".app-item").forEach((item) => {
|
||||
item.classList.toggle("active", item.getAttribute("href") === hash);
|
||||
});
|
||||
if (settingsPanel) settingsPanel.classList.remove("show");
|
||||
}
|
||||
});
|
||||
|
||||
// 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", () => {
|
||||
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");
|
||||
});
|
||||
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");
|
||||
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");
|
||||
const setting = el.id.replace("toggle-", "");
|
||||
localStorage.setItem(`gb-${setting}`, el.classList.contains("active"));
|
||||
el.classList.toggle("active");
|
||||
const setting = el.id.replace("toggle-", "");
|
||||
localStorage.setItem(`gb-${setting}`, el.classList.contains("active"));
|
||||
}
|
||||
|
||||
// Load quick toggle states
|
||||
["notifications", "sound", "compact"].forEach((setting) => {
|
||||
const saved = localStorage.getItem(`gb-${setting}`);
|
||||
const toggle = document.getElementById(`toggle-${setting}`);
|
||||
if (toggle && saved !== null) {
|
||||
toggle.classList.toggle("active", saved === "true");
|
||||
}
|
||||
const saved = localStorage.getItem(`gb-${setting}`);
|
||||
const toggle = document.getElementById(`toggle-${setting}`);
|
||||
if (toggle && saved !== null) {
|
||||
toggle.classList.toggle("active", saved === "true");
|
||||
}
|
||||
});
|
||||
|
||||
// Show keyboard shortcuts notification
|
||||
function showKeyboardShortcuts() {
|
||||
window.showNotification(
|
||||
"Alt+1-9,0 for apps, Alt+A Admin, Alt+M Monitoring, Alt+S Settings, Alt+, quick settings",
|
||||
"info",
|
||||
8000
|
||||
);
|
||||
window.showNotification(
|
||||
"Alt+1-9,0 for apps, Alt+A Admin, Alt+M Monitoring, Alt+S Settings, Alt+, quick settings",
|
||||
"info",
|
||||
8000,
|
||||
);
|
||||
}
|
||||
|
||||
// Accessibility: Announce page changes to screen readers
|
||||
function announceToScreenReader(message) {
|
||||
const liveRegion = document.getElementById("aria-live");
|
||||
if (liveRegion) {
|
||||
liveRegion.textContent = message;
|
||||
// Clear after announcement
|
||||
setTimeout(() => {
|
||||
liveRegion.textContent = "";
|
||||
}, 1000);
|
||||
}
|
||||
const liveRegion = document.getElementById("aria-live");
|
||||
if (liveRegion) {
|
||||
liveRegion.textContent = message;
|
||||
// Clear after announcement
|
||||
setTimeout(() => {
|
||||
liveRegion.textContent = "";
|
||||
}, 1000);
|
||||
}
|
||||
}
|
||||
|
||||
// HTMX accessibility hooks
|
||||
document.body.addEventListener("htmx:beforeRequest", function (e) {
|
||||
const target = e.detail.target;
|
||||
if (target && target.id === "main-content") {
|
||||
target.setAttribute("aria-busy", "true");
|
||||
announceToScreenReader("Loading content...");
|
||||
}
|
||||
const target = e.detail.target;
|
||||
if (target && target.id === "main-content") {
|
||||
target.setAttribute("aria-busy", "true");
|
||||
announceToScreenReader("Loading content...");
|
||||
}
|
||||
});
|
||||
|
||||
document.body.addEventListener("htmx:afterSwap", function (e) {
|
||||
const target = e.detail.target;
|
||||
if (target && target.id === "main-content") {
|
||||
target.setAttribute("aria-busy", "false");
|
||||
// Focus management: move focus to main content after navigation
|
||||
target.focus();
|
||||
announceToScreenReader("Content loaded");
|
||||
}
|
||||
const target = e.detail.target;
|
||||
if (target && target.id === "main-content") {
|
||||
target.setAttribute("aria-busy", "false");
|
||||
// Focus management: move focus to main content after navigation
|
||||
target.focus();
|
||||
announceToScreenReader("Content loaded");
|
||||
}
|
||||
});
|
||||
|
||||
document.body.addEventListener("htmx:responseError", function (e) {
|
||||
const target = e.detail.target;
|
||||
if (target) {
|
||||
target.setAttribute("aria-busy", "false");
|
||||
}
|
||||
announceToScreenReader("Error loading content. Please try again.");
|
||||
const target = e.detail.target;
|
||||
if (target) {
|
||||
target.setAttribute("aria-busy", "false");
|
||||
}
|
||||
announceToScreenReader("Error loading content. Please try again.");
|
||||
});
|
||||
|
||||
// Keyboard navigation for apps grid
|
||||
document.addEventListener("keydown", function (e) {
|
||||
const appsGrid = document.querySelector(".apps-grid");
|
||||
if (!appsGrid || !appsGrid.closest(".show")) return;
|
||||
const appsGrid = document.querySelector(".apps-grid");
|
||||
if (!appsGrid || !appsGrid.closest(".show")) return;
|
||||
|
||||
const items = Array.from(appsGrid.querySelectorAll(".app-item"));
|
||||
const currentIndex = items.findIndex((item) => item === document.activeElement);
|
||||
const items = Array.from(appsGrid.querySelectorAll(".app-item"));
|
||||
const currentIndex = items.findIndex(
|
||||
(item) => item === document.activeElement,
|
||||
);
|
||||
|
||||
if (currentIndex === -1) return;
|
||||
if (currentIndex === -1) return;
|
||||
|
||||
let newIndex = currentIndex;
|
||||
const columns = 3; // Grid has 3 columns on desktop
|
||||
let newIndex = currentIndex;
|
||||
const columns = 3; // Grid has 3 columns on desktop
|
||||
|
||||
switch (e.key) {
|
||||
case "ArrowRight":
|
||||
newIndex = Math.min(currentIndex + 1, items.length - 1);
|
||||
break;
|
||||
case "ArrowLeft":
|
||||
newIndex = Math.max(currentIndex - 1, 0);
|
||||
break;
|
||||
case "ArrowDown":
|
||||
newIndex = Math.min(currentIndex + columns, items.length - 1);
|
||||
break;
|
||||
case "ArrowUp":
|
||||
newIndex = Math.max(currentIndex - columns, 0);
|
||||
break;
|
||||
case "Home":
|
||||
newIndex = 0;
|
||||
break;
|
||||
case "End":
|
||||
newIndex = items.length - 1;
|
||||
break;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
switch (e.key) {
|
||||
case "ArrowRight":
|
||||
newIndex = Math.min(currentIndex + 1, items.length - 1);
|
||||
break;
|
||||
case "ArrowLeft":
|
||||
newIndex = Math.max(currentIndex - 1, 0);
|
||||
break;
|
||||
case "ArrowDown":
|
||||
newIndex = Math.min(currentIndex + columns, items.length - 1);
|
||||
break;
|
||||
case "ArrowUp":
|
||||
newIndex = Math.max(currentIndex - columns, 0);
|
||||
break;
|
||||
case "Home":
|
||||
newIndex = 0;
|
||||
break;
|
||||
case "End":
|
||||
newIndex = items.length - 1;
|
||||
break;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
|
||||
if (newIndex !== currentIndex) {
|
||||
e.preventDefault();
|
||||
items[newIndex].focus();
|
||||
}
|
||||
if (newIndex !== currentIndex) {
|
||||
e.preventDefault();
|
||||
items[newIndex].focus();
|
||||
}
|
||||
});
|
||||
|
||||
// Notification System
|
||||
window.showNotification = function (message, type = "info", duration = 5000) {
|
||||
const container = document.getElementById("notifications");
|
||||
if (!container) return;
|
||||
const container = document.getElementById("notifications");
|
||||
if (!container) return;
|
||||
|
||||
const notification = document.createElement("div");
|
||||
notification.className = `notification ${type}`;
|
||||
notification.innerHTML = `
|
||||
const notification = document.createElement("div");
|
||||
notification.className = `notification ${type}`;
|
||||
notification.innerHTML = `
|
||||
<div class="notification-content">
|
||||
<div class="notification-message">${message}</div>
|
||||
</div>
|
||||
<button class="notification-close" onclick="this.parentElement.remove()" style="background:none;border:none;color:var(--text-secondary);cursor:pointer;font-size:1.25rem;">×</button>
|
||||
`;
|
||||
container.appendChild(notification);
|
||||
if (duration > 0) {
|
||||
setTimeout(() => notification.remove(), duration);
|
||||
}
|
||||
container.appendChild(notification);
|
||||
if (duration > 0) {
|
||||
setTimeout(() => notification.remove(), duration);
|
||||
}
|
||||
};
|
||||
|
||||
// Global HTMX error handling with retry mechanism
|
||||
const htmxRetryConfig = {
|
||||
maxRetries: 3,
|
||||
retryDelay: 1000,
|
||||
retryCount: new Map(),
|
||||
maxRetries: 3,
|
||||
retryDelay: 1000,
|
||||
retryCount: new Map(),
|
||||
};
|
||||
|
||||
function getRetryKey(elt) {
|
||||
return (
|
||||
elt.getAttribute("hx-get") ||
|
||||
elt.getAttribute("hx-post") ||
|
||||
elt.getAttribute("hx-put") ||
|
||||
elt.getAttribute("hx-delete") ||
|
||||
elt.id ||
|
||||
Math.random().toString(36)
|
||||
);
|
||||
return (
|
||||
elt.getAttribute("hx-get") ||
|
||||
elt.getAttribute("hx-post") ||
|
||||
elt.getAttribute("hx-put") ||
|
||||
elt.getAttribute("hx-delete") ||
|
||||
elt.id ||
|
||||
Math.random().toString(36)
|
||||
);
|
||||
}
|
||||
|
||||
function showErrorState(target, errorMessage, retryCallback) {
|
||||
const errorHtml = `
|
||||
const errorHtml = `
|
||||
<div class="error-state">
|
||||
<svg class="error-state-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<circle cx="12" cy="12" r="10"></circle>
|
||||
|
|
@ -280,95 +322,108 @@ function showErrorState(target, errorMessage, retryCallback) {
|
|||
</div>
|
||||
</div>
|
||||
`;
|
||||
target.innerHTML = errorHtml;
|
||||
target.dataset.retryCallback = retryCallback;
|
||||
target.innerHTML = errorHtml;
|
||||
target.dataset.retryCallback = retryCallback;
|
||||
}
|
||||
|
||||
window.retryLastRequest = function (btn) {
|
||||
const target = btn.closest(".error-state").parentElement;
|
||||
const retryCallback = target.dataset.retryCallback;
|
||||
const target = btn.closest(".error-state").parentElement;
|
||||
const retryCallback = target.dataset.retryCallback;
|
||||
|
||||
btn.disabled = true;
|
||||
btn.innerHTML = '<span class="spinner"></span> Retrying...';
|
||||
btn.disabled = true;
|
||||
btn.innerHTML = '<span class="spinner"></span> Retrying...';
|
||||
|
||||
if (retryCallback && window[retryCallback]) {
|
||||
window[retryCallback]();
|
||||
if (retryCallback && window[retryCallback]) {
|
||||
window[retryCallback]();
|
||||
} else {
|
||||
// Try to re-trigger HTMX request
|
||||
const triggers = target.querySelectorAll("[hx-get], [hx-post]");
|
||||
if (triggers.length > 0) {
|
||||
htmx.trigger(triggers[0], "htmx:trigger");
|
||||
} else {
|
||||
// Try to re-trigger HTMX request
|
||||
const triggers = target.querySelectorAll("[hx-get], [hx-post]");
|
||||
if (triggers.length > 0) {
|
||||
htmx.trigger(triggers[0], "htmx:trigger");
|
||||
} else {
|
||||
// Reload the current app
|
||||
const activeApp = document.querySelector(".app-item.active");
|
||||
if (activeApp) {
|
||||
activeApp.click();
|
||||
}
|
||||
}
|
||||
// Reload the current app
|
||||
const activeApp = document.querySelector(".app-item.active");
|
||||
if (activeApp) {
|
||||
activeApp.click();
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Handle HTMX errors globally
|
||||
document.body.addEventListener("htmx:responseError", function (e) {
|
||||
const target = e.detail.target;
|
||||
const xhr = e.detail.xhr;
|
||||
const retryKey = getRetryKey(e.detail.elt);
|
||||
const target = e.detail.target;
|
||||
const xhr = e.detail.xhr;
|
||||
const retryKey = getRetryKey(e.detail.elt);
|
||||
|
||||
let currentRetries = htmxRetryConfig.retryCount.get(retryKey) || 0;
|
||||
let currentRetries = htmxRetryConfig.retryCount.get(retryKey) || 0;
|
||||
|
||||
// Auto-retry for network errors (status 0) or server errors (5xx)
|
||||
if ((xhr.status === 0 || xhr.status >= 500) && currentRetries < htmxRetryConfig.maxRetries) {
|
||||
htmxRetryConfig.retryCount.set(retryKey, currentRetries + 1);
|
||||
const delay = htmxRetryConfig.retryDelay * Math.pow(2, currentRetries);
|
||||
// Auto-retry for network errors (status 0) or server errors (5xx)
|
||||
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
|
||||
);
|
||||
window.showNotification(
|
||||
`Request failed. Retrying in ${delay / 1000}s... (${currentRetries + 1}/${htmxRetryConfig.maxRetries})`,
|
||||
"warning",
|
||||
delay,
|
||||
);
|
||||
|
||||
setTimeout(() => {
|
||||
htmx.trigger(e.detail.elt, "htmx:trigger");
|
||||
}, delay);
|
||||
} else {
|
||||
// Max retries reached or client error - show error state
|
||||
htmxRetryConfig.retryCount.delete(retryKey);
|
||||
setTimeout(() => {
|
||||
htmx.trigger(e.detail.elt, "htmx:trigger");
|
||||
}, delay);
|
||||
} else {
|
||||
// Max retries reached or client error - show error state
|
||||
htmxRetryConfig.retryCount.delete(retryKey);
|
||||
|
||||
let errorMessage = "We couldn't load the content.";
|
||||
if (xhr.status === 401) {
|
||||
errorMessage = "Your session has expired. Please log in again.";
|
||||
} else if (xhr.status === 403) {
|
||||
errorMessage = "You don't have permission to access this resource.";
|
||||
} 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.";
|
||||
} else if (xhr.status === 0) {
|
||||
errorMessage = "Unable to connect. Please check your internet connection.";
|
||||
}
|
||||
|
||||
if (target && target.id === "main-content") {
|
||||
showErrorState(target, errorMessage);
|
||||
} else {
|
||||
window.showNotification(errorMessage, "error", 8000);
|
||||
}
|
||||
let errorMessage = "We couldn't load the content.";
|
||||
if (xhr.status === 401) {
|
||||
errorMessage = "Your session has expired. Please log in again.";
|
||||
} else if (xhr.status === 403) {
|
||||
errorMessage = "You don't have permission to access this resource.";
|
||||
} 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.";
|
||||
} else if (xhr.status === 0) {
|
||||
errorMessage =
|
||||
"Unable to connect. Please check your internet connection.";
|
||||
}
|
||||
|
||||
if (target && target.id === "main-content") {
|
||||
showErrorState(target, errorMessage);
|
||||
} else {
|
||||
window.showNotification(errorMessage, "error", 8000);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Clear retry count on successful request
|
||||
document.body.addEventListener("htmx:afterRequest", function (e) {
|
||||
if (e.detail.successful) {
|
||||
const retryKey = getRetryKey(e.detail.elt);
|
||||
htmxRetryConfig.retryCount.delete(retryKey);
|
||||
}
|
||||
if (e.detail.successful) {
|
||||
const retryKey = getRetryKey(e.detail.elt);
|
||||
htmxRetryConfig.retryCount.delete(retryKey);
|
||||
}
|
||||
});
|
||||
|
||||
// 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,
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,98 +1,118 @@
|
|||
// HTMX-based application initialization
|
||||
(function() {
|
||||
'use strict';
|
||||
(function () {
|
||||
"use strict";
|
||||
|
||||
// Configuration
|
||||
const config = {
|
||||
wsUrl: '/ws',
|
||||
apiBase: '/api',
|
||||
reconnectDelay: 3000,
|
||||
maxReconnectAttempts: 5
|
||||
};
|
||||
// Configuration
|
||||
const config = {
|
||||
wsUrl: "/ws",
|
||||
apiBase: "/api",
|
||||
reconnectDelay: 3000,
|
||||
maxReconnectAttempts: 5,
|
||||
};
|
||||
|
||||
// State
|
||||
let reconnectAttempts = 0;
|
||||
let wsConnection = null;
|
||||
// State
|
||||
let reconnectAttempts = 0;
|
||||
let wsConnection = null;
|
||||
|
||||
// Initialize HTMX extensions
|
||||
function initHTMX() {
|
||||
// Configure HTMX
|
||||
htmx.config.defaultSwapStyle = 'innerHTML';
|
||||
htmx.config.defaultSettleDelay = 100;
|
||||
htmx.config.timeout = 10000;
|
||||
// Initialize HTMX extensions
|
||||
function initHTMX() {
|
||||
// Configure HTMX
|
||||
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');
|
||||
if (token) {
|
||||
event.detail.headers['X-CSRF-Token'] = token;
|
||||
}
|
||||
});
|
||||
// Add CSRF token to all requests if available
|
||||
document.body.addEventListener("htmx:configRequest", (event) => {
|
||||
const token = localStorage.getItem("csrf_token");
|
||||
if (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');
|
||||
});
|
||||
// Handle errors globally
|
||||
document.body.addEventListener("htmx:responseError", (event) => {
|
||||
console.error("HTMX Error:", event.detail);
|
||||
showNotification("Connection error. Please try again.", "error");
|
||||
});
|
||||
|
||||
// Handle successful swaps
|
||||
document.body.addEventListener('htmx:afterSwap', (event) => {
|
||||
// Auto-scroll messages if in chat
|
||||
const messages = document.getElementById('messages');
|
||||
if (messages && event.detail.target === messages) {
|
||||
messages.scrollTop = messages.scrollHeight;
|
||||
}
|
||||
});
|
||||
// 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;
|
||||
|
||||
// Handle WebSocket messages
|
||||
document.body.addEventListener('htmx:wsMessage', (event) => {
|
||||
handleWebSocketMessage(JSON.parse(event.detail.message));
|
||||
});
|
||||
// If target doesn't exist or response is 404, prevent the swap
|
||||
if (!target || status === 404) {
|
||||
event.detail.shouldSwap = false;
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle WebSocket connection events
|
||||
document.body.addEventListener('htmx:wsConnecting', () => {
|
||||
updateConnectionStatus('connecting');
|
||||
});
|
||||
// For empty responses, set empty content to prevent insertBefore errors
|
||||
if (
|
||||
!event.detail.serverResponse ||
|
||||
event.detail.serverResponse.trim() === ""
|
||||
) {
|
||||
event.detail.serverResponse = "<!-- empty -->";
|
||||
}
|
||||
});
|
||||
|
||||
document.body.addEventListener('htmx:wsOpen', () => {
|
||||
updateConnectionStatus('connected');
|
||||
reconnectAttempts = 0;
|
||||
});
|
||||
|
||||
document.body.addEventListener('htmx:wsClose', () => {
|
||||
updateConnectionStatus('disconnected');
|
||||
attemptReconnect();
|
||||
});
|
||||
}
|
||||
// Handle successful swaps
|
||||
document.body.addEventListener("htmx:afterSwap", (event) => {
|
||||
// Auto-scroll messages if in chat
|
||||
const messages = document.getElementById("messages");
|
||||
if (messages && event.detail.target === messages) {
|
||||
messages.scrollTop = messages.scrollHeight;
|
||||
}
|
||||
});
|
||||
|
||||
// Handle WebSocket messages
|
||||
function handleWebSocketMessage(message) {
|
||||
switch(message.type) {
|
||||
case 'message':
|
||||
appendMessage(message);
|
||||
break;
|
||||
case 'notification':
|
||||
showNotification(message.text, message.severity);
|
||||
break;
|
||||
case 'status':
|
||||
updateStatus(message);
|
||||
break;
|
||||
case 'suggestion':
|
||||
addSuggestion(message.text);
|
||||
break;
|
||||
default:
|
||||
console.log('Unknown message type:', message.type);
|
||||
}
|
||||
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:wsOpen", () => {
|
||||
updateConnectionStatus("connected");
|
||||
reconnectAttempts = 0;
|
||||
});
|
||||
|
||||
document.body.addEventListener("htmx:wsClose", () => {
|
||||
updateConnectionStatus("disconnected");
|
||||
attemptReconnect();
|
||||
});
|
||||
}
|
||||
|
||||
// Handle WebSocket messages
|
||||
function handleWebSocketMessage(message) {
|
||||
switch (message.type) {
|
||||
case "message":
|
||||
appendMessage(message);
|
||||
break;
|
||||
case "notification":
|
||||
showNotification(message.text, message.severity);
|
||||
break;
|
||||
case "status":
|
||||
updateStatus(message);
|
||||
break;
|
||||
case "suggestion":
|
||||
addSuggestion(message.text);
|
||||
break;
|
||||
default:
|
||||
console.log("Unknown message type:", message.type);
|
||||
}
|
||||
}
|
||||
|
||||
// Append message to chat
|
||||
function appendMessage(message) {
|
||||
const messagesEl = document.getElementById('messages');
|
||||
if (!messagesEl) return;
|
||||
// Append message to chat
|
||||
function appendMessage(message) {
|
||||
const messagesEl = document.getElementById("messages");
|
||||
if (!messagesEl) return;
|
||||
|
||||
const messageEl = document.createElement('div');
|
||||
messageEl.className = `message ${message.sender === 'user' ? 'user' : 'bot'}`;
|
||||
messageEl.innerHTML = `
|
||||
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>
|
||||
<span class="text">${escapeHtml(message.text)}</span>
|
||||
|
|
@ -100,216 +120,218 @@
|
|||
</div>
|
||||
`;
|
||||
|
||||
messagesEl.appendChild(messageEl);
|
||||
messagesEl.scrollTop = messagesEl.scrollHeight;
|
||||
messagesEl.appendChild(messageEl);
|
||||
messagesEl.scrollTop = messagesEl.scrollHeight;
|
||||
}
|
||||
|
||||
// Add suggestion chip
|
||||
function addSuggestion(text) {
|
||||
const suggestionsEl = document.getElementById("suggestions");
|
||||
if (!suggestionsEl) return;
|
||||
|
||||
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");
|
||||
|
||||
suggestionsEl.appendChild(chip);
|
||||
htmx.process(chip);
|
||||
}
|
||||
|
||||
// Update connection status
|
||||
function updateConnectionStatus(status) {
|
||||
const statusEl = document.getElementById("connectionStatus");
|
||||
if (!statusEl) return;
|
||||
|
||||
statusEl.className = `connection-status ${status}`;
|
||||
statusEl.textContent = status.charAt(0).toUpperCase() + status.slice(1);
|
||||
}
|
||||
|
||||
// Update general status
|
||||
function updateStatus(message) {
|
||||
const statusEl = document.getElementById("status-" + message.id);
|
||||
if (statusEl) {
|
||||
statusEl.textContent = message.text;
|
||||
statusEl.className = `status ${message.severity}`;
|
||||
}
|
||||
}
|
||||
|
||||
// Show notification
|
||||
function showNotification(text, type = "info") {
|
||||
const notification = document.createElement("div");
|
||||
notification.className = `notification ${type}`;
|
||||
notification.textContent = text;
|
||||
|
||||
const container = document.getElementById("notifications") || document.body;
|
||||
container.appendChild(notification);
|
||||
|
||||
setTimeout(() => {
|
||||
notification.classList.add("fade-out");
|
||||
setTimeout(() => notification.remove(), 300);
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
// Attempt to reconnect WebSocket
|
||||
function attemptReconnect() {
|
||||
if (reconnectAttempts >= config.maxReconnectAttempts) {
|
||||
showNotification("Connection lost. Please refresh the page.", "error");
|
||||
return;
|
||||
}
|
||||
|
||||
// Add suggestion chip
|
||||
function addSuggestion(text) {
|
||||
const suggestionsEl = document.getElementById('suggestions');
|
||||
if (!suggestionsEl) return;
|
||||
reconnectAttempts++;
|
||||
setTimeout(() => {
|
||||
console.log(`Reconnection attempt ${reconnectAttempts}...`);
|
||||
htmx.trigger(document.body, "htmx:wsReconnect");
|
||||
}, config.reconnectDelay);
|
||||
}
|
||||
|
||||
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');
|
||||
// Utility: Escape HTML
|
||||
function escapeHtml(text) {
|
||||
const div = document.createElement("div");
|
||||
div.textContent = text;
|
||||
return div.innerHTML;
|
||||
}
|
||||
|
||||
suggestionsEl.appendChild(chip);
|
||||
htmx.process(chip);
|
||||
}
|
||||
// Utility: Format timestamp
|
||||
function formatTime(timestamp) {
|
||||
if (!timestamp) return "";
|
||||
const date = new Date(timestamp);
|
||||
return date.toLocaleTimeString("en-US", {
|
||||
hour: "numeric",
|
||||
minute: "2-digit",
|
||||
hour12: true,
|
||||
});
|
||||
}
|
||||
|
||||
// Update connection status
|
||||
function updateConnectionStatus(status) {
|
||||
const statusEl = document.getElementById('connectionStatus');
|
||||
if (!statusEl) return;
|
||||
// Handle navigation
|
||||
function initNavigation() {
|
||||
// Update active nav item on page change
|
||||
document.addEventListener("htmx:pushedIntoHistory", (event) => {
|
||||
const path = event.detail.path;
|
||||
updateActiveNav(path);
|
||||
});
|
||||
|
||||
statusEl.className = `connection-status ${status}`;
|
||||
statusEl.textContent = status.charAt(0).toUpperCase() + status.slice(1);
|
||||
}
|
||||
// Handle browser back/forward
|
||||
window.addEventListener("popstate", (event) => {
|
||||
updateActiveNav(window.location.pathname);
|
||||
});
|
||||
}
|
||||
|
||||
// Update general status
|
||||
function updateStatus(message) {
|
||||
const statusEl = document.getElementById('status-' + message.id);
|
||||
if (statusEl) {
|
||||
statusEl.textContent = message.text;
|
||||
statusEl.className = `status ${message.severity}`;
|
||||
// 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");
|
||||
} else {
|
||||
item.classList.remove("active");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Initialize keyboard shortcuts
|
||||
function initKeyboardShortcuts() {
|
||||
document.addEventListener("keydown", (e) => {
|
||||
// Send message on Enter (when in input)
|
||||
if (e.key === "Enter" && !e.shiftKey) {
|
||||
const input = document.getElementById("messageInput");
|
||||
if (input && document.activeElement === input) {
|
||||
e.preventDefault();
|
||||
const form = input.closest("form");
|
||||
if (form) {
|
||||
htmx.trigger(form, "submit");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Show notification
|
||||
function showNotification(text, type = 'info') {
|
||||
const notification = document.createElement('div');
|
||||
notification.className = `notification ${type}`;
|
||||
notification.textContent = text;
|
||||
// Focus input on /
|
||||
if (e.key === "/" && document.activeElement.tagName !== "INPUT") {
|
||||
e.preventDefault();
|
||||
const input = document.getElementById("messageInput");
|
||||
if (input) input.focus();
|
||||
}
|
||||
|
||||
const container = document.getElementById('notifications') || document.body;
|
||||
container.appendChild(notification);
|
||||
|
||||
setTimeout(() => {
|
||||
notification.classList.add('fade-out');
|
||||
setTimeout(() => notification.remove(), 300);
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
// Attempt to reconnect WebSocket
|
||||
function attemptReconnect() {
|
||||
if (reconnectAttempts >= config.maxReconnectAttempts) {
|
||||
showNotification('Connection lost. Please refresh the page.', 'error');
|
||||
return;
|
||||
// Escape to blur input
|
||||
if (e.key === "Escape") {
|
||||
const input = document.getElementById("messageInput");
|
||||
if (input && document.activeElement === input) {
|
||||
input.blur();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
reconnectAttempts++;
|
||||
setTimeout(() => {
|
||||
console.log(`Reconnection attempt ${reconnectAttempts}...`);
|
||||
htmx.trigger(document.body, 'htmx:wsReconnect');
|
||||
}, config.reconnectDelay);
|
||||
}
|
||||
// Initialize scroll behavior
|
||||
function initScrollBehavior() {
|
||||
const scrollBtn = document.getElementById("scrollToBottom");
|
||||
const messages = document.getElementById("messages");
|
||||
|
||||
// Utility: Escape HTML
|
||||
function escapeHtml(text) {
|
||||
const div = document.createElement('div');
|
||||
div.textContent = text;
|
||||
return div.innerHTML;
|
||||
}
|
||||
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";
|
||||
});
|
||||
|
||||
// Utility: Format timestamp
|
||||
function formatTime(timestamp) {
|
||||
if (!timestamp) return '';
|
||||
const date = new Date(timestamp);
|
||||
return date.toLocaleTimeString('en-US', {
|
||||
hour: 'numeric',
|
||||
minute: '2-digit',
|
||||
hour12: true
|
||||
// Scroll to bottom on click
|
||||
scrollBtn.addEventListener("click", () => {
|
||||
messages.scrollTo({
|
||||
top: messages.scrollHeight,
|
||||
behavior: "smooth",
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Handle navigation
|
||||
function initNavigation() {
|
||||
// Update active nav item on page change
|
||||
document.addEventListener('htmx:pushedIntoHistory', (event) => {
|
||||
const path = event.detail.path;
|
||||
updateActiveNav(path);
|
||||
});
|
||||
|
||||
// Handle browser back/forward
|
||||
window.addEventListener('popstate', (event) => {
|
||||
updateActiveNav(window.location.pathname);
|
||||
});
|
||||
// Initialize theme if ThemeManager exists
|
||||
function initTheme() {
|
||||
if (window.ThemeManager) {
|
||||
ThemeManager.init();
|
||||
}
|
||||
}
|
||||
|
||||
// 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');
|
||||
} else {
|
||||
item.classList.remove('active');
|
||||
}
|
||||
});
|
||||
}
|
||||
// Main initialization
|
||||
function init() {
|
||||
console.log("Initializing HTMX application...");
|
||||
|
||||
// Initialize HTMX
|
||||
initHTMX();
|
||||
|
||||
// Initialize navigation
|
||||
initNavigation();
|
||||
|
||||
// Initialize keyboard shortcuts
|
||||
function initKeyboardShortcuts() {
|
||||
document.addEventListener('keydown', (e) => {
|
||||
// Send message on Enter (when in input)
|
||||
if (e.key === 'Enter' && !e.shiftKey) {
|
||||
const input = document.getElementById('messageInput');
|
||||
if (input && document.activeElement === input) {
|
||||
e.preventDefault();
|
||||
const form = input.closest('form');
|
||||
if (form) {
|
||||
htmx.trigger(form, 'submit');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Focus input on /
|
||||
if (e.key === '/' && document.activeElement.tagName !== 'INPUT') {
|
||||
e.preventDefault();
|
||||
const input = document.getElementById('messageInput');
|
||||
if (input) input.focus();
|
||||
}
|
||||
|
||||
// Escape to blur input
|
||||
if (e.key === 'Escape') {
|
||||
const input = document.getElementById('messageInput');
|
||||
if (input && document.activeElement === input) {
|
||||
input.blur();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
initKeyboardShortcuts();
|
||||
|
||||
// Initialize scroll behavior
|
||||
function initScrollBehavior() {
|
||||
const scrollBtn = document.getElementById('scrollToBottom');
|
||||
const messages = document.getElementById('messages');
|
||||
initScrollBehavior();
|
||||
|
||||
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';
|
||||
});
|
||||
// Initialize theme
|
||||
initTheme();
|
||||
|
||||
// Scroll to bottom on click
|
||||
scrollBtn.addEventListener('click', () => {
|
||||
messages.scrollTo({
|
||||
top: messages.scrollHeight,
|
||||
behavior: 'smooth'
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
// Set initial active nav
|
||||
updateActiveNav(window.location.pathname);
|
||||
|
||||
// Initialize theme if ThemeManager exists
|
||||
function initTheme() {
|
||||
if (window.ThemeManager) {
|
||||
ThemeManager.init();
|
||||
}
|
||||
}
|
||||
console.log("HTMX application initialized");
|
||||
}
|
||||
|
||||
// Main initialization
|
||||
function init() {
|
||||
console.log('Initializing HTMX application...');
|
||||
// Wait for DOM and HTMX to be ready
|
||||
if (document.readyState === "loading") {
|
||||
document.addEventListener("DOMContentLoaded", init);
|
||||
} else {
|
||||
init();
|
||||
}
|
||||
|
||||
// Initialize HTMX
|
||||
initHTMX();
|
||||
|
||||
// Initialize navigation
|
||||
initNavigation();
|
||||
|
||||
// Initialize keyboard shortcuts
|
||||
initKeyboardShortcuts();
|
||||
|
||||
// Initialize scroll behavior
|
||||
initScrollBehavior();
|
||||
|
||||
// Initialize theme
|
||||
initTheme();
|
||||
|
||||
// Set initial active nav
|
||||
updateActiveNav(window.location.pathname);
|
||||
|
||||
console.log('HTMX application initialized');
|
||||
}
|
||||
|
||||
// Wait for DOM and HTMX to be ready
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', init);
|
||||
} else {
|
||||
init();
|
||||
}
|
||||
|
||||
// Expose public API
|
||||
window.BotServerApp = {
|
||||
showNotification,
|
||||
appendMessage,
|
||||
updateConnectionStatus,
|
||||
config
|
||||
};
|
||||
// Expose public API
|
||||
window.BotServerApp = {
|
||||
showNotification,
|
||||
appendMessage,
|
||||
updateConnectionStatus,
|
||||
config,
|
||||
};
|
||||
})();
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,75 +1,78 @@
|
|||
/* 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: ''
|
||||
};
|
||||
let logsWs = null;
|
||||
// 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: "",
|
||||
};
|
||||
var logsWs = null;
|
||||
}
|
||||
|
||||
function initLogsWebSocket() {
|
||||
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
||||
logsWs = new WebSocket(`${protocol}//${window.location.host}/ws/logs`);
|
||||
const protocol = window.location.protocol === "https:" ? "wss:" : "ws:";
|
||||
logsWs = new WebSocket(`${protocol}//${window.location.host}/ws/logs`);
|
||||
|
||||
logsWs.onopen = function() {
|
||||
updateLogsConnectionStatus('connected', 'Connected');
|
||||
};
|
||||
logsWs.onopen = function () {
|
||||
updateLogsConnectionStatus("connected", "Connected");
|
||||
};
|
||||
|
||||
logsWs.onclose = function() {
|
||||
updateLogsConnectionStatus('disconnected', 'Disconnected');
|
||||
// Reconnect after 3 seconds
|
||||
setTimeout(initLogsWebSocket, 3000);
|
||||
};
|
||||
logsWs.onclose = function () {
|
||||
updateLogsConnectionStatus("disconnected", "Disconnected");
|
||||
// Reconnect after 3 seconds
|
||||
setTimeout(initLogsWebSocket, 3000);
|
||||
};
|
||||
|
||||
logsWs.onerror = function() {
|
||||
updateLogsConnectionStatus('disconnected', 'Error');
|
||||
};
|
||||
logsWs.onerror = function () {
|
||||
updateLogsConnectionStatus("disconnected", "Error");
|
||||
};
|
||||
|
||||
logsWs.onmessage = function(event) {
|
||||
if (!isStreaming) return;
|
||||
logsWs.onmessage = function (event) {
|
||||
if (!isStreaming) return;
|
||||
|
||||
try {
|
||||
const logData = JSON.parse(event.data);
|
||||
appendLog(logData);
|
||||
} catch (e) {
|
||||
console.error('Failed to parse log message:', e);
|
||||
}
|
||||
};
|
||||
try {
|
||||
const logData = JSON.parse(event.data);
|
||||
appendLog(logData);
|
||||
} catch (e) {
|
||||
console.error("Failed to parse log message:", e);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function updateLogsConnectionStatus(status, text) {
|
||||
const statusEl = document.getElementById('connection-status');
|
||||
if (statusEl) {
|
||||
statusEl.className = `connection-status ${status}`;
|
||||
statusEl.querySelector('.status-text').textContent = text;
|
||||
}
|
||||
const statusEl = document.getElementById("connection-status");
|
||||
if (statusEl) {
|
||||
statusEl.className = `connection-status ${status}`;
|
||||
statusEl.querySelector(".status-text").textContent = text;
|
||||
}
|
||||
}
|
||||
|
||||
function appendLog(log) {
|
||||
const stream = document.getElementById('log-stream');
|
||||
if (!stream) return;
|
||||
const stream = document.getElementById("log-stream");
|
||||
if (!stream) return;
|
||||
|
||||
const placeholder = stream.querySelector('.log-placeholder');
|
||||
if (placeholder) {
|
||||
placeholder.remove();
|
||||
}
|
||||
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';
|
||||
entry.dataset.id = log.id || Date.now();
|
||||
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 = `
|
||||
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>
|
||||
|
|
@ -77,134 +80,142 @@ function appendLog(log) {
|
|||
</button>
|
||||
`;
|
||||
|
||||
// Store full log data for detail view
|
||||
entry._logData = log;
|
||||
// Store full log data for detail view
|
||||
entry._logData = log;
|
||||
|
||||
// Apply current filters
|
||||
if (!matchesLogFilters(entry)) {
|
||||
entry.classList.add('hidden');
|
||||
}
|
||||
// Apply current filters
|
||||
if (!matchesLogFilters(entry)) {
|
||||
entry.classList.add("hidden");
|
||||
}
|
||||
|
||||
stream.appendChild(entry);
|
||||
stream.appendChild(entry);
|
||||
|
||||
// Update counts
|
||||
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');
|
||||
if (totalEl) {
|
||||
totalEl.textContent = Object.values(logCounts).reduce((a, b) => a + b, 0);
|
||||
}
|
||||
// Update counts
|
||||
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");
|
||||
if (totalEl) {
|
||||
totalEl.textContent = Object.values(logCounts).reduce((a, b) => a + b, 0);
|
||||
}
|
||||
|
||||
// Auto-scroll to bottom
|
||||
if (autoScroll) {
|
||||
stream.scrollTop = stream.scrollHeight;
|
||||
}
|
||||
// Auto-scroll to bottom
|
||||
if (autoScroll) {
|
||||
stream.scrollTop = stream.scrollHeight;
|
||||
}
|
||||
|
||||
// Limit log entries to prevent memory issues
|
||||
const maxEntries = 1000;
|
||||
while (stream.children.length > maxEntries) {
|
||||
const removed = stream.firstChild;
|
||||
if (removed._logData) {
|
||||
const removedLevel = removed._logData.level || 'info';
|
||||
if (logCounts[removedLevel] > 0) {
|
||||
logCounts[removedLevel]--;
|
||||
}
|
||||
}
|
||||
stream.removeChild(removed);
|
||||
// Limit log entries to prevent memory issues
|
||||
const maxEntries = 1000;
|
||||
while (stream.children.length > maxEntries) {
|
||||
const removed = stream.firstChild;
|
||||
if (removed._logData) {
|
||||
const removedLevel = removed._logData.level || "info";
|
||||
if (logCounts[removedLevel] > 0) {
|
||||
logCounts[removedLevel]--;
|
||||
}
|
||||
}
|
||||
stream.removeChild(removed);
|
||||
}
|
||||
}
|
||||
|
||||
function formatLogTimestamp(timestamp) {
|
||||
if (!timestamp) return '--';
|
||||
const date = new Date(timestamp);
|
||||
return date.toISOString().replace('T', ' ').slice(0, 23);
|
||||
if (!timestamp) return "--";
|
||||
const date = new Date(timestamp);
|
||||
return date.toISOString().replace("T", " ").slice(0, 23);
|
||||
}
|
||||
|
||||
function escapeLogHtml(text) {
|
||||
const div = document.createElement('div');
|
||||
div.textContent = text;
|
||||
return div.innerHTML;
|
||||
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) {
|
||||
return false;
|
||||
}
|
||||
// Level filter
|
||||
if (
|
||||
currentFilters.level !== "all" &&
|
||||
entry.dataset.level !== currentFilters.level
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Service filter
|
||||
if (currentFilters.service !== 'all' && entry.dataset.service !== currentFilters.service) {
|
||||
return false;
|
||||
}
|
||||
// Service filter
|
||||
if (
|
||||
currentFilters.service !== "all" &&
|
||||
entry.dataset.service !== currentFilters.service
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Search filter
|
||||
if (currentFilters.search) {
|
||||
const text = entry.textContent.toLowerCase();
|
||||
if (!text.includes(currentFilters.search.toLowerCase())) {
|
||||
return false;
|
||||
}
|
||||
// Search filter
|
||||
if (currentFilters.search) {
|
||||
const text = entry.textContent.toLowerCase();
|
||||
if (!text.includes(currentFilters.search.toLowerCase())) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
return true;
|
||||
}
|
||||
|
||||
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 => {
|
||||
if (matchesLogFilters(entry)) {
|
||||
entry.classList.remove('hidden');
|
||||
} else {
|
||||
entry.classList.add('hidden');
|
||||
}
|
||||
});
|
||||
const entries = document.querySelectorAll(".log-entry");
|
||||
entries.forEach((entry) => {
|
||||
if (matchesLogFilters(entry)) {
|
||||
entry.classList.remove("hidden");
|
||||
} else {
|
||||
entry.classList.add("hidden");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function debounceLogSearch(value) {
|
||||
clearTimeout(searchDebounceTimer);
|
||||
searchDebounceTimer = setTimeout(() => {
|
||||
currentFilters.search = value;
|
||||
applyLogFilters();
|
||||
}, 300);
|
||||
clearTimeout(searchDebounceTimer);
|
||||
searchDebounceTimer = setTimeout(() => {
|
||||
currentFilters.search = value;
|
||||
applyLogFilters();
|
||||
}, 300);
|
||||
}
|
||||
|
||||
function toggleStream() {
|
||||
isStreaming = !isStreaming;
|
||||
const btn = document.getElementById('stream-toggle');
|
||||
if (!btn) return;
|
||||
isStreaming = !isStreaming;
|
||||
const btn = document.getElementById("stream-toggle");
|
||||
if (!btn) return;
|
||||
|
||||
if (isStreaming) {
|
||||
btn.classList.remove('paused');
|
||||
btn.innerHTML = `
|
||||
if (isStreaming) {
|
||||
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>
|
||||
<rect x="14" y="4" width="4" height="16"></rect>
|
||||
</svg>
|
||||
<span>Pause</span>
|
||||
`;
|
||||
} else {
|
||||
btn.classList.add('paused');
|
||||
btn.innerHTML = `
|
||||
} else {
|
||||
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>
|
||||
</svg>
|
||||
<span>Resume</span>
|
||||
`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function clearLogs() {
|
||||
if (confirm('Are you sure you want to clear all logs?')) {
|
||||
const stream = document.getElementById('log-stream');
|
||||
if (!stream) return;
|
||||
if (confirm("Are you sure you want to clear all logs?")) {
|
||||
const stream = document.getElementById("log-stream");
|
||||
if (!stream) return;
|
||||
|
||||
stream.innerHTML = `
|
||||
stream.innerHTML = `
|
||||
<div class="log-placeholder">
|
||||
<svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
|
||||
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path>
|
||||
|
|
@ -218,119 +229,129 @@ function clearLogs() {
|
|||
</div>
|
||||
`;
|
||||
|
||||
// Reset counts
|
||||
logCounts = { debug: 0, info: 0, warn: 0, error: 0, fatal: 0 };
|
||||
Object.keys(logCounts).forEach(level => {
|
||||
const el = document.getElementById(`${level}-count`);
|
||||
if (el) el.textContent = '0';
|
||||
});
|
||||
const totalEl = document.getElementById('total-count');
|
||||
if (totalEl) totalEl.textContent = '0';
|
||||
}
|
||||
// Reset counts
|
||||
logCounts = { debug: 0, info: 0, warn: 0, error: 0, fatal: 0 };
|
||||
Object.keys(logCounts).forEach((level) => {
|
||||
const el = document.getElementById(`${level}-count`);
|
||||
if (el) el.textContent = "0";
|
||||
});
|
||||
const totalEl = document.getElementById("total-count");
|
||||
if (totalEl) totalEl.textContent = "0";
|
||||
}
|
||||
}
|
||||
|
||||
function downloadLogs() {
|
||||
const entries = document.querySelectorAll('.log-entry');
|
||||
let logs = [];
|
||||
const entries = document.querySelectorAll(".log-entry");
|
||||
let logs = [];
|
||||
|
||||
entries.forEach(entry => {
|
||||
if (entry._logData) {
|
||||
logs.push(entry._logData);
|
||||
}
|
||||
});
|
||||
entries.forEach((entry) => {
|
||||
if (entry._logData) {
|
||||
logs.push(entry._logData);
|
||||
}
|
||||
});
|
||||
|
||||
const blob = new Blob([JSON.stringify(logs, null, 2)], { type: 'application/json' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = `logs-${new Date().toISOString().slice(0, 19).replace(/[T:]/g, '-')}.json`;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
URL.revokeObjectURL(url);
|
||||
const blob = new Blob([JSON.stringify(logs, null, 2)], {
|
||||
type: "application/json",
|
||||
});
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement("a");
|
||||
a.href = url;
|
||||
a.download = `logs-${new Date().toISOString().slice(0, 19).replace(/[T:]/g, "-")}.json`;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
|
||||
function scrollToTop() {
|
||||
const stream = document.getElementById('log-stream');
|
||||
if (stream) {
|
||||
stream.scrollTop = 0;
|
||||
autoScroll = false;
|
||||
updateLogScrollButtons();
|
||||
}
|
||||
const stream = document.getElementById("log-stream");
|
||||
if (stream) {
|
||||
stream.scrollTop = 0;
|
||||
autoScroll = false;
|
||||
updateLogScrollButtons();
|
||||
}
|
||||
}
|
||||
|
||||
function scrollToBottom() {
|
||||
const stream = document.getElementById('log-stream');
|
||||
if (stream) {
|
||||
stream.scrollTop = stream.scrollHeight;
|
||||
autoScroll = true;
|
||||
updateLogScrollButtons();
|
||||
}
|
||||
const stream = document.getElementById("log-stream");
|
||||
if (stream) {
|
||||
stream.scrollTop = stream.scrollHeight;
|
||||
autoScroll = true;
|
||||
updateLogScrollButtons();
|
||||
}
|
||||
}
|
||||
|
||||
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 logData = entry._logData || {
|
||||
timestamp: entry.querySelector('.log-timestamp').textContent,
|
||||
level: entry.dataset.level,
|
||||
service: entry.dataset.service,
|
||||
message: entry.querySelector('.log-message').textContent
|
||||
};
|
||||
const entry = btn.closest(".log-entry");
|
||||
const logData = entry._logData || {
|
||||
timestamp: entry.querySelector(".log-timestamp").textContent,
|
||||
level: entry.dataset.level,
|
||||
service: entry.dataset.service,
|
||||
message: entry.querySelector(".log-message").textContent,
|
||||
};
|
||||
|
||||
const panel = document.getElementById('log-detail-panel');
|
||||
const content = document.getElementById('log-detail-content');
|
||||
if (!panel || !content) return;
|
||||
const panel = document.getElementById("log-detail-panel");
|
||||
const content = document.getElementById("log-detail-content");
|
||||
if (!panel || !content) return;
|
||||
|
||||
content.innerHTML = `
|
||||
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() {
|
||||
// Initialize WebSocket connection if on logs page
|
||||
if (document.getElementById('log-stream')) {
|
||||
initLogsWebSocket();
|
||||
}
|
||||
document.addEventListener("DOMContentLoaded", function () {
|
||||
// Initialize WebSocket connection if on logs page
|
||||
if (document.getElementById("log-stream")) {
|
||||
initLogsWebSocket();
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,67 +1,75 @@
|
|||
/* Monitoring module - shared/base JavaScript */
|
||||
|
||||
function setActiveNav(element) {
|
||||
document.querySelectorAll('.monitoring-nav .nav-item').forEach(item => {
|
||||
item.classList.remove('active');
|
||||
});
|
||||
element.classList.add('active');
|
||||
document.querySelectorAll(".monitoring-nav .nav-item").forEach((item) => {
|
||||
item.classList.remove("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;
|
||||
// Update page 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);
|
||||
// Store selected time range
|
||||
localStorage.setItem("monitoring-time-range", range);
|
||||
|
||||
// Trigger refresh of current view
|
||||
htmx.trigger('#monitoring-content', 'refresh');
|
||||
// Trigger refresh of current view
|
||||
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);
|
||||
// Visual feedback
|
||||
const btn = event.currentTarget;
|
||||
btn.classList.add("active");
|
||||
setTimeout(() => btn.classList.remove("active"), 500);
|
||||
}
|
||||
|
||||
let autoRefresh = true;
|
||||
function toggleAutoRefresh() {
|
||||
autoRefresh = !autoRefresh;
|
||||
const btn = document.getElementById('auto-refresh-btn');
|
||||
btn.classList.toggle('active', autoRefresh);
|
||||
// Guard against duplicate declarations on HTMX reload
|
||||
if (typeof window.monitoringModuleInitialized === "undefined") {
|
||||
window.monitoringModuleInitialized = true;
|
||||
var autoRefresh = true;
|
||||
}
|
||||
|
||||
if (autoRefresh) {
|
||||
// Re-enable polling by refreshing the page content
|
||||
htmx.trigger('#monitoring-content', 'refresh');
|
||||
}
|
||||
function toggleAutoRefresh() {
|
||||
autoRefresh = !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");
|
||||
}
|
||||
}
|
||||
|
||||
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() {
|
||||
// Restore time range preference
|
||||
const savedRange = localStorage.getItem('monitoring-time-range');
|
||||
if (savedRange) {
|
||||
const timeRangeEl = document.getElementById('time-range');
|
||||
if (timeRangeEl) timeRangeEl.value = savedRange;
|
||||
}
|
||||
document.addEventListener("DOMContentLoaded", function () {
|
||||
// Restore time range preference
|
||||
const savedRange = localStorage.getItem("monitoring-time-range");
|
||||
if (savedRange) {
|
||||
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);
|
||||
// Set auto-refresh button state
|
||||
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>';
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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
379
ui/suite/tasks/intents.html
Normal 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>
|
||||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -7,13 +7,16 @@
|
|||
// STATE MANAGEMENT
|
||||
// =============================================================================
|
||||
|
||||
const TasksState = {
|
||||
selectedTaskId: 2, // Default selected task
|
||||
currentFilter: "complete",
|
||||
tasks: [],
|
||||
wsConnection: null,
|
||||
agentLogPaused: false,
|
||||
};
|
||||
// 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
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue