Update: UI files and add error-reporter.js
This commit is contained in:
parent
5e10222a94
commit
5618ed4367
11 changed files with 1625 additions and 1299 deletions
|
|
@ -7,10 +7,10 @@ use axum::{
|
||||||
http::{Request, StatusCode},
|
http::{Request, StatusCode},
|
||||||
response::{Html, IntoResponse, Response},
|
response::{Html, IntoResponse, Response},
|
||||||
routing::{any, get},
|
routing::{any, get},
|
||||||
Router,
|
Json, Router,
|
||||||
};
|
};
|
||||||
use futures_util::{SinkExt, StreamExt};
|
use futures_util::{SinkExt, StreamExt};
|
||||||
use log::{debug, error, info};
|
use log::{debug, error, info, warn};
|
||||||
#[cfg(feature = "embed-ui")]
|
#[cfg(feature = "embed-ui")]
|
||||||
use rust_embed::RustEmbed;
|
use rust_embed::RustEmbed;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
|
|
@ -130,8 +130,98 @@ const ROOT_FILES: &[&str] = &[
|
||||||
"single.gbui",
|
"single.gbui",
|
||||||
];
|
];
|
||||||
|
|
||||||
pub async fn index() -> impl IntoResponse {
|
pub async fn index(OriginalUri(uri): OriginalUri) -> Response {
|
||||||
serve_suite().await
|
let path = uri.path();
|
||||||
|
|
||||||
|
// Check if path contains static asset directories - serve them directly
|
||||||
|
let path_lower = path.to_lowercase();
|
||||||
|
if path_lower.contains("/js/")
|
||||||
|
|| path_lower.contains("/css/")
|
||||||
|
|| path_lower.contains("/vendor/")
|
||||||
|
|| path_lower.contains("/assets/")
|
||||||
|
|| path_lower.contains("/public/")
|
||||||
|
|| path_lower.contains("/partials/")
|
||||||
|
|| path_lower.ends_with(".js")
|
||||||
|
|| path_lower.ends_with(".css")
|
||||||
|
|| path_lower.ends_with(".png")
|
||||||
|
|| path_lower.ends_with(".jpg")
|
||||||
|
|| path_lower.ends_with(".jpeg")
|
||||||
|
|| path_lower.ends_with(".gif")
|
||||||
|
|| path_lower.ends_with(".svg")
|
||||||
|
|| path_lower.ends_with(".ico")
|
||||||
|
|| path_lower.ends_with(".woff")
|
||||||
|
|| path_lower.ends_with(".woff2")
|
||||||
|
|| path_lower.ends_with(".ttf")
|
||||||
|
|| path_lower.ends_with(".eot")
|
||||||
|
|| path_lower.ends_with(".mp4")
|
||||||
|
|| path_lower.ends_with(".webm")
|
||||||
|
|| path_lower.ends_with(".mp3")
|
||||||
|
|| path_lower.ends_with(".wav")
|
||||||
|
{
|
||||||
|
// Remove bot name prefix if present (e.g., /edu/suite/js/file.js -> suite/js/file.js)
|
||||||
|
let path_parts: Vec<&str> = path.split('/').collect();
|
||||||
|
let fs_path = if path_parts.len() > 1 {
|
||||||
|
let mut start_idx = 1;
|
||||||
|
let known_dirs = ["suite", "js", "css", "vendor", "assets", "public", "partials", "settings", "auth", "about", "drive", "chat", "tasks", "admin", "mail", "calendar", "meet", "docs", "sheet", "slides", "paper", "research", "sources", "learn", "analytics", "dashboards", "monitoring", "people", "crm", "tickets", "billing", "products", "video", "player", "canvas", "social", "project", "goals", "workspace", "designer"];
|
||||||
|
|
||||||
|
if path_parts.len() > start_idx && !known_dirs.contains(&path_parts[start_idx]) {
|
||||||
|
start_idx += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
path_parts[start_idx..].join("/")
|
||||||
|
} else {
|
||||||
|
path.to_string()
|
||||||
|
};
|
||||||
|
|
||||||
|
let full_path = get_ui_root().join(&fs_path);
|
||||||
|
|
||||||
|
debug!("index: Serving static file: {} -> {:?} (fs_path: {})", path, full_path, fs_path);
|
||||||
|
|
||||||
|
#[cfg(feature = "embed-ui")]
|
||||||
|
{
|
||||||
|
let asset_path = fs_path.trim_start_matches('/');
|
||||||
|
if let Some(content) = Assets::get(asset_path) {
|
||||||
|
let mime = mime_guess::from_path(asset_path).first_or_octet_stream();
|
||||||
|
return ([(axum::http::header::CONTENT_TYPE, mime.as_ref())], content.data).into_response();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(feature = "embed-ui"))]
|
||||||
|
{
|
||||||
|
if let Ok(bytes) = tokio::fs::read(&full_path).await {
|
||||||
|
let mime = mime_guess::from_path(&full_path).first_or_octet_stream();
|
||||||
|
return (StatusCode::OK, [("content-type", mime.as_ref())], bytes).into_response();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
warn!("index: Static file not found: {} -> {:?}", path, full_path);
|
||||||
|
return StatusCode::NOT_FOUND.into_response();
|
||||||
|
}
|
||||||
|
|
||||||
|
let path_parts: Vec<&str> = path.split('/').collect();
|
||||||
|
let bot_name = path_parts
|
||||||
|
.iter()
|
||||||
|
.rev()
|
||||||
|
.find(|part| {
|
||||||
|
!part.is_empty()
|
||||||
|
&& **part != "chat"
|
||||||
|
&& **part != "app"
|
||||||
|
&& **part != "ws"
|
||||||
|
&& **part != "ui"
|
||||||
|
&& **part != "api"
|
||||||
|
&& **part != "auth"
|
||||||
|
&& **part != "suite"
|
||||||
|
&& !part.ends_with(".js")
|
||||||
|
&& !part.ends_with(".css")
|
||||||
|
})
|
||||||
|
.map(|s| s.to_string());
|
||||||
|
|
||||||
|
info!(
|
||||||
|
"index: Extracted bot_name: {:?} from path: {}",
|
||||||
|
bot_name,
|
||||||
|
path
|
||||||
|
);
|
||||||
|
serve_suite(bot_name).await.into_response()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_ui_root() -> PathBuf {
|
pub fn get_ui_root() -> PathBuf {
|
||||||
|
|
@ -196,7 +286,7 @@ pub async fn serve_minimal() -> impl IntoResponse {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn serve_suite() -> impl IntoResponse {
|
pub async fn serve_suite(bot_name: Option<String>) -> impl IntoResponse {
|
||||||
let raw_html_res = {
|
let raw_html_res = {
|
||||||
#[cfg(feature = "embed-ui")]
|
#[cfg(feature = "embed-ui")]
|
||||||
{
|
{
|
||||||
|
|
@ -235,6 +325,32 @@ pub async fn serve_suite() -> impl IntoResponse {
|
||||||
#[allow(unused_mut)] // Mutable required for feature-gated blocks
|
#[allow(unused_mut)] // Mutable required for feature-gated blocks
|
||||||
let mut html = raw_html;
|
let mut html = raw_html;
|
||||||
|
|
||||||
|
// Inject base tag and bot_name into the page
|
||||||
|
if let Some(head_end) = html.find("</head>") {
|
||||||
|
// Set base href to include bot context if present (e.g., /edu/)
|
||||||
|
let base_href = if let Some(ref name) = bot_name {
|
||||||
|
format!("/{}/", name)
|
||||||
|
} else {
|
||||||
|
"/".to_string()
|
||||||
|
};
|
||||||
|
let base_tag = format!(r#"<base href="{}">"#, base_href);
|
||||||
|
html.insert_str(head_end, &base_tag);
|
||||||
|
|
||||||
|
if let Some(name) = bot_name {
|
||||||
|
info!("serve_suite: Injecting bot_name '{}' into page with base href='{}'", name, base_href);
|
||||||
|
let bot_script = format!(
|
||||||
|
r#"<script>window.__INITIAL_BOT_NAME__ = "{}";</script>"#,
|
||||||
|
&name
|
||||||
|
);
|
||||||
|
html.insert_str(head_end + base_tag.len(), &bot_script);
|
||||||
|
info!("serve_suite: Successfully injected base tag and bot_name script");
|
||||||
|
} else {
|
||||||
|
info!("serve_suite: Successfully injected base tag (no bot_name)");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
error!("serve_suite: Failed to find </head> tag to inject content");
|
||||||
|
}
|
||||||
|
|
||||||
// Core Apps
|
// Core Apps
|
||||||
#[cfg(not(feature = "chat"))]
|
#[cfg(not(feature = "chat"))]
|
||||||
{
|
{
|
||||||
|
|
@ -452,14 +568,26 @@ async fn health(State(state): State<AppState>) -> (StatusCode, axum::Json<serde_
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn api_health() -> (StatusCode, axum::Json<serde_json::Value>) {
|
async fn api_health(State(state): State<AppState>) -> (StatusCode, axum::Json<serde_json::Value>) {
|
||||||
|
if state.health_check().await {
|
||||||
(
|
(
|
||||||
StatusCode::OK,
|
StatusCode::OK,
|
||||||
axum::Json(serde_json::json!({
|
axum::Json(serde_json::json!({
|
||||||
"status": "ok",
|
"status": "ok",
|
||||||
|
"botserver": "healthy",
|
||||||
"version": env!("CARGO_PKG_VERSION")
|
"version": env!("CARGO_PKG_VERSION")
|
||||||
})),
|
})),
|
||||||
)
|
)
|
||||||
|
} else {
|
||||||
|
(
|
||||||
|
StatusCode::SERVICE_UNAVAILABLE,
|
||||||
|
axum::Json(serde_json::json!({
|
||||||
|
"status": "error",
|
||||||
|
"botserver": "unhealthy",
|
||||||
|
"version": env!("CARGO_PKG_VERSION")
|
||||||
|
})),
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn extract_app_context(headers: &axum::http::HeaderMap, path: &str) -> Option<String> {
|
fn extract_app_context(headers: &axum::http::HeaderMap, path: &str) -> Option<String> {
|
||||||
|
|
@ -588,6 +716,7 @@ async fn build_proxy_response(resp: reqwest::Response) -> Response<Body> {
|
||||||
fn create_api_router() -> Router<AppState> {
|
fn create_api_router() -> Router<AppState> {
|
||||||
Router::new()
|
Router::new()
|
||||||
.route("/health", get(api_health))
|
.route("/health", get(api_health))
|
||||||
|
.route("/client-error", axum::routing::post(handle_client_error))
|
||||||
.fallback(any(proxy_api))
|
.fallback(any(proxy_api))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -598,6 +727,35 @@ struct WsQuery {
|
||||||
bot_name: Option<String>,
|
bot_name: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
struct ClientError {
|
||||||
|
message: String,
|
||||||
|
stack: Option<String>,
|
||||||
|
source: String,
|
||||||
|
url: String,
|
||||||
|
user_agent: String,
|
||||||
|
timestamp: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn handle_client_error(Json(error): Json<ClientError>) -> impl IntoResponse {
|
||||||
|
warn!(
|
||||||
|
"CLIENT:{}: {} at {} ({}) - {}",
|
||||||
|
error.source.to_uppercase(),
|
||||||
|
error.message,
|
||||||
|
error.url,
|
||||||
|
error.timestamp,
|
||||||
|
error.user_agent
|
||||||
|
);
|
||||||
|
|
||||||
|
if let Some(stack) = &error.stack {
|
||||||
|
if !stack.is_empty() {
|
||||||
|
warn!("CLIENT:STACK: {}", stack);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
StatusCode::OK
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Default, Deserialize)]
|
#[derive(Debug, Default, Deserialize)]
|
||||||
struct OptionalWsQuery {
|
struct OptionalWsQuery {
|
||||||
task_id: Option<String>,
|
task_id: Option<String>,
|
||||||
|
|
@ -613,11 +771,14 @@ async fn ws_proxy(
|
||||||
let path_parts: Vec<&str> = uri.path().split('/').collect();
|
let path_parts: Vec<&str> = uri.path().split('/').collect();
|
||||||
let bot_name = params
|
let bot_name = params
|
||||||
.bot_name
|
.bot_name
|
||||||
|
.filter(|name| name != "ws" && !name.is_empty())
|
||||||
.or_else(|| {
|
.or_else(|| {
|
||||||
// Try to extract from path like /edu or /app/edu
|
// Try to extract from path like /edu or /app/edu
|
||||||
path_parts
|
path_parts
|
||||||
.iter()
|
.iter()
|
||||||
.find(|part| !part.is_empty() && *part != "chat" && *part != "app")
|
.find(|part| {
|
||||||
|
!part.is_empty() && **part != "chat" && **part != "app" && **part != "ws"
|
||||||
|
})
|
||||||
.map(|s| s.to_string())
|
.map(|s| s.to_string())
|
||||||
})
|
})
|
||||||
.unwrap_or_else(|| "default".to_string());
|
.unwrap_or_else(|| "default".to_string());
|
||||||
|
|
@ -996,10 +1157,12 @@ fn add_static_routes(router: Router<AppState>, _suite_path: &Path) -> Router<App
|
||||||
#[cfg(not(feature = "embed-ui"))]
|
#[cfg(not(feature = "embed-ui"))]
|
||||||
{
|
{
|
||||||
let mut r = router;
|
let mut r = router;
|
||||||
// Serve suite directories ONLY through /suite/{dir} path
|
// Serve suite directories at BOTH root level and /suite/{dir} path
|
||||||
// This prevents duplicate routes that cause ServeDir to return index.html for file requests
|
// This allows HTML files to reference js/vendor/file.js directly
|
||||||
for dir in SUITE_DIRS {
|
for dir in SUITE_DIRS {
|
||||||
let path = _suite_path.join(dir);
|
let path = _suite_path.join(dir);
|
||||||
|
info!("Adding route for /{} -> {:?}", dir, path);
|
||||||
|
r = r.nest_service(&format!("/{dir}"), ServeDir::new(path.clone()));
|
||||||
info!("Adding route for /suite/{} -> {:?}", dir, path);
|
info!("Adding route for /suite/{} -> {:?}", dir, path);
|
||||||
r = r.nest_service(&format!("/suite/{dir}"), ServeDir::new(path.clone()));
|
r = r.nest_service(&format!("/suite/{dir}"), ServeDir::new(path.clone()));
|
||||||
}
|
}
|
||||||
|
|
@ -1024,13 +1187,14 @@ pub fn configure_router() -> Router {
|
||||||
.nest("/ui", create_ui_router())
|
.nest("/ui", create_ui_router())
|
||||||
.nest("/ws", create_ws_router())
|
.nest("/ws", create_ws_router())
|
||||||
.nest("/apps", create_apps_router())
|
.nest("/apps", create_apps_router())
|
||||||
.route("/", get(index))
|
.route("/favicon.ico", get(serve_favicon));
|
||||||
.route("/minimal", get(serve_minimal))
|
|
||||||
.route("/suite", get(serve_suite))
|
|
||||||
.route("/favicon.ico", get(serve_favicon))
|
|
||||||
.nest_service("/auth", ServeDir::new(suite_path.join("auth")));
|
|
||||||
|
|
||||||
router = add_static_routes(router, &suite_path);
|
router = add_static_routes(router, &suite_path);
|
||||||
|
|
||||||
router.fallback(get(index)).with_state(state)
|
router
|
||||||
|
.route("/", get(index))
|
||||||
|
.route("/minimal", get(serve_minimal))
|
||||||
|
.route("/suite", get(serve_suite))
|
||||||
|
.fallback(get(index))
|
||||||
|
.with_state(state)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,9 @@
|
||||||
<script src="/js/vendor/htmx-ws.js"></script>
|
<script src="/js/vendor/htmx-ws.js"></script>
|
||||||
<script src="/js/vendor/htmx-json-enc.js"></script>
|
<script src="/js/vendor/htmx-json-enc.js"></script>
|
||||||
|
|
||||||
|
<!-- ERROR REPORTER - Captures JS errors and sends to server log -->
|
||||||
|
<script src="/js/error-reporter.js"></script>
|
||||||
|
|
||||||
<!-- i18n -->
|
<!-- i18n -->
|
||||||
<script src="/js/i18n.js"></script>
|
<script src="/js/i18n.js"></script>
|
||||||
|
|
||||||
|
|
@ -116,7 +119,7 @@
|
||||||
href="#tasks"
|
href="#tasks"
|
||||||
class="app-item"
|
class="app-item"
|
||||||
role="menuitem"
|
role="menuitem"
|
||||||
hx-get="/tasks/tasks.html"
|
hx-get="/suite/tasks/autotask.html"
|
||||||
hx-target="#main-content"
|
hx-target="#main-content"
|
||||||
hx-push-url="true"
|
hx-push-url="true"
|
||||||
>
|
>
|
||||||
|
|
|
||||||
|
|
@ -324,7 +324,7 @@
|
||||||
<a
|
<a
|
||||||
href="#tasks"
|
href="#tasks"
|
||||||
class="app-card"
|
class="app-card"
|
||||||
hx-get="/suite/tasks/tasks.html"
|
hx-get="/suite/tasks/autotask.html"
|
||||||
hx-target="#main-content"
|
hx-target="#main-content"
|
||||||
hx-push-url="/#tasks"
|
hx-push-url="/#tasks"
|
||||||
>
|
>
|
||||||
|
|
|
||||||
|
|
@ -351,8 +351,8 @@
|
||||||
this.clearAuth();
|
this.clearAuth();
|
||||||
this.emit("tokenExpired");
|
this.emit("tokenExpired");
|
||||||
|
|
||||||
const currentPath = window.location.pathname;
|
const currentPath = window.location.pathname + window.location.hash;
|
||||||
if (!currentPath.startsWith("/auth/")) {
|
if (!window.location.pathname.startsWith("/auth/")) {
|
||||||
window.location.href =
|
window.location.href =
|
||||||
"/auth/login.html?expired=1&redirect=" +
|
"/auth/login.html?expired=1&redirect=" +
|
||||||
encodeURIComponent(currentPath);
|
encodeURIComponent(currentPath);
|
||||||
|
|
|
||||||
135
ui/suite/js/error-reporter.js
Normal file
135
ui/suite/js/error-reporter.js
Normal file
|
|
@ -0,0 +1,135 @@
|
||||||
|
(function() {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const MAX_ERRORS = 50;
|
||||||
|
const REPORT_ENDPOINT = '/api/client-errors';
|
||||||
|
let errorQueue = [];
|
||||||
|
let isReporting = false;
|
||||||
|
|
||||||
|
function formatError(error, context = {}) {
|
||||||
|
return {
|
||||||
|
type: error.name || 'Error',
|
||||||
|
message: error.message || String(error),
|
||||||
|
stack: error.stack,
|
||||||
|
url: window.location.href,
|
||||||
|
userAgent: navigator.userAgent,
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
context: context
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function reportErrors() {
|
||||||
|
if (isReporting || errorQueue.length === 0) return;
|
||||||
|
|
||||||
|
isReporting = true;
|
||||||
|
const errorsToReport = errorQueue.splice(0, MAX_ERRORS);
|
||||||
|
errorQueue = [];
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(REPORT_ENDPOINT, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ errors: errorsToReport })
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
console.warn('[ErrorReporter] Failed to send errors:', response.status);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('[ErrorReporter] Failed to send errors:', e.message);
|
||||||
|
errorQueue.unshift(...errorsToReport);
|
||||||
|
} finally {
|
||||||
|
isReporting = false;
|
||||||
|
|
||||||
|
if (errorQueue.length > 0) {
|
||||||
|
setTimeout(reportErrors, 1000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function queueError(errorData) {
|
||||||
|
errorQueue.push(errorData);
|
||||||
|
if (errorQueue.length >= 10) {
|
||||||
|
reportErrors();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
window.addEventListener('error', (event) => {
|
||||||
|
const errorData = formatError(event.error || new Error(event.message), {
|
||||||
|
filename: event.filename,
|
||||||
|
lineno: event.lineno,
|
||||||
|
colno: event.colno
|
||||||
|
});
|
||||||
|
queueError(errorData);
|
||||||
|
});
|
||||||
|
|
||||||
|
window.addEventListener('unhandledrejection', (event) => {
|
||||||
|
const errorData = formatError(event.reason || new Error(String(event.reason)), {
|
||||||
|
type: 'unhandledRejection'
|
||||||
|
});
|
||||||
|
queueError(errorData);
|
||||||
|
});
|
||||||
|
|
||||||
|
window.ErrorReporter = {
|
||||||
|
report: function(error, context) {
|
||||||
|
queueError(formatError(error, context));
|
||||||
|
},
|
||||||
|
flush: function() {
|
||||||
|
reportErrors();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (document.readyState === 'complete') {
|
||||||
|
setTimeout(reportErrors, 1000);
|
||||||
|
} else {
|
||||||
|
window.addEventListener('load', () => {
|
||||||
|
setTimeout(reportErrors, 1000);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('[ErrorReporter] Client-side error reporting initialized');
|
||||||
|
|
||||||
|
window.NavigationLogger = {
|
||||||
|
log: function(from, to, method) {
|
||||||
|
const navEvent = {
|
||||||
|
type: 'navigation',
|
||||||
|
from: from,
|
||||||
|
to: to,
|
||||||
|
method: method,
|
||||||
|
url: window.location.href,
|
||||||
|
timestamp: new Date().toISOString()
|
||||||
|
};
|
||||||
|
|
||||||
|
queueError({
|
||||||
|
name: 'Navigation',
|
||||||
|
message: `${method}: ${from} -> ${to}`,
|
||||||
|
stack: undefined
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
document.body.addEventListener('click', function(e) {
|
||||||
|
const target = e.target.closest('[data-section]');
|
||||||
|
if (target) {
|
||||||
|
const section = target.getAttribute('data-section');
|
||||||
|
const currentHash = window.location.hash.slice(1) || '';
|
||||||
|
if (section !== currentHash) {
|
||||||
|
setTimeout(() => {
|
||||||
|
window.NavigationLogger.log(currentHash || 'home', section, 'click');
|
||||||
|
}, 100);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, true);
|
||||||
|
|
||||||
|
window.addEventListener('hashchange', function(e) {
|
||||||
|
const oldURL = new URL(e.oldURL);
|
||||||
|
const newURL = new URL(e.newURL);
|
||||||
|
const fromHash = oldURL.hash.slice(1) || '';
|
||||||
|
const toHash = newURL.hash.slice(1) || '';
|
||||||
|
window.NavigationLogger.log(fromHash || 'home', toHash, 'hashchange');
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('[NavigationLogger] Navigation tracking initialized');
|
||||||
|
})();
|
||||||
|
|
@ -203,7 +203,12 @@
|
||||||
|
|
||||||
// Handle WebSocket messages
|
// Handle WebSocket messages
|
||||||
function handleWebSocketMessage(message) {
|
function handleWebSocketMessage(message) {
|
||||||
switch (message.type) {
|
const messageType = message.type || message.event;
|
||||||
|
|
||||||
|
// Debug logging
|
||||||
|
console.log("handleWebSocketMessage called with:", { messageType, message });
|
||||||
|
|
||||||
|
switch (messageType) {
|
||||||
case "message":
|
case "message":
|
||||||
appendMessage(message);
|
appendMessage(message);
|
||||||
break;
|
break;
|
||||||
|
|
@ -216,8 +221,28 @@
|
||||||
case "suggestion":
|
case "suggestion":
|
||||||
addSuggestion(message.text);
|
addSuggestion(message.text);
|
||||||
break;
|
break;
|
||||||
|
case "change_theme":
|
||||||
|
console.log("Processing change_theme event, not appending to chat");
|
||||||
|
if (message.data) {
|
||||||
|
ThemeManager.setThemeFromServer(message.data);
|
||||||
|
|
||||||
|
if (message.data.color1 || message.data.color2) {
|
||||||
|
const root = document.documentElement;
|
||||||
|
if (message.data.color1)
|
||||||
|
root.style.setProperty("--color1", message.data.color1);
|
||||||
|
if (message.data.color2)
|
||||||
|
root.style.setProperty("--color2", message.data.color2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return; // Don't append theme events to chat
|
||||||
default:
|
default:
|
||||||
console.log("Unknown message type:", message.type);
|
// Only append unknown message types to chat if they have text content
|
||||||
|
if (message.text || message.content) {
|
||||||
|
console.log("Unknown message type, treating as chat message:", messageType);
|
||||||
|
appendMessage(message);
|
||||||
|
} else {
|
||||||
|
console.log("Unknown message type:", messageType, message);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -295,6 +295,12 @@
|
||||||
});
|
});
|
||||||
|
|
||||||
window.addEventListener("gb:auth:expired", function (event) {
|
window.addEventListener("gb:auth:expired", function (event) {
|
||||||
|
// Check if current bot is public - if so, skip redirect
|
||||||
|
if (window.__BOT_IS_PUBLIC__ === true) {
|
||||||
|
console.log("[GBSecurity] Bot is public, skipping auth redirect");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
console.log(
|
console.log(
|
||||||
"[GBSecurity] Auth expired, clearing tokens and redirecting",
|
"[GBSecurity] Auth expired, clearing tokens and redirecting",
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -15,11 +15,11 @@ window.Suite = {
|
||||||
description: "",
|
description: "",
|
||||||
actions: [],
|
actions: [],
|
||||||
searchable: true,
|
searchable: true,
|
||||||
...config
|
...config,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Trigger UI update if Omnibox is initialized
|
// Trigger UI update if Omnibox is initialized
|
||||||
if (typeof Omnibox !== 'undefined' && Omnibox.isActive) {
|
if (typeof Omnibox !== "undefined" && Omnibox.isActive) {
|
||||||
Omnibox.updateActions();
|
Omnibox.updateActions();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
@ -36,7 +36,7 @@ window.Suite = {
|
||||||
getContextActions(contextId) {
|
getContextActions(contextId) {
|
||||||
const app = this.apps.get(contextId);
|
const app = this.apps.get(contextId);
|
||||||
return app ? app.actions : null;
|
return app ? app.actions : null;
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
// ==========================================
|
// ==========================================
|
||||||
|
|
@ -55,13 +55,9 @@ const Omnibox = {
|
||||||
this.backdrop = document.getElementById("omniboxBackdrop");
|
this.backdrop = document.getElementById("omniboxBackdrop");
|
||||||
this.results = document.getElementById("omniboxResults");
|
this.results = document.getElementById("omniboxResults");
|
||||||
this.chat = document.getElementById("omniboxChat");
|
this.chat = document.getElementById("omniboxChat");
|
||||||
this.chatMessages = document.getElementById(
|
this.chatMessages = document.getElementById("omniboxChatMessages");
|
||||||
"omniboxChatMessages",
|
this.chatInput = document.getElementById("omniboxChatInput");
|
||||||
);
|
this.modeToggle = document.getElementById("omniboxModeToggle");
|
||||||
this.chatInput =
|
|
||||||
document.getElementById("omniboxChatInput");
|
|
||||||
this.modeToggle =
|
|
||||||
document.getElementById("omniboxModeToggle");
|
|
||||||
|
|
||||||
this.bindEvents();
|
this.bindEvents();
|
||||||
},
|
},
|
||||||
|
|
@ -77,9 +73,7 @@ const Omnibox = {
|
||||||
);
|
);
|
||||||
|
|
||||||
// Keyboard navigation
|
// Keyboard navigation
|
||||||
this.input.addEventListener("keydown", (e) =>
|
this.input.addEventListener("keydown", (e) => this.handleKeydown(e));
|
||||||
this.handleKeydown(e),
|
|
||||||
);
|
|
||||||
this.chatInput?.addEventListener("keydown", (e) => {
|
this.chatInput?.addEventListener("keydown", (e) => {
|
||||||
if (e.key === "Enter" && !e.shiftKey) {
|
if (e.key === "Enter" && !e.shiftKey) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
@ -94,12 +88,8 @@ const Omnibox = {
|
||||||
});
|
});
|
||||||
|
|
||||||
// Action buttons
|
// Action buttons
|
||||||
document
|
document.querySelectorAll(".omnibox-action").forEach((btn) => {
|
||||||
.querySelectorAll(".omnibox-action")
|
btn.addEventListener("click", () => this.handleAction(btn));
|
||||||
.forEach((btn) => {
|
|
||||||
btn.addEventListener("click", () =>
|
|
||||||
this.handleAction(btn),
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Send button
|
// Send button
|
||||||
|
|
@ -115,9 +105,7 @@ const Omnibox = {
|
||||||
// Expand button
|
// Expand button
|
||||||
document
|
document
|
||||||
.getElementById("omniboxExpandBtn")
|
.getElementById("omniboxExpandBtn")
|
||||||
?.addEventListener("click", () =>
|
?.addEventListener("click", () => this.expandToFullChat());
|
||||||
this.expandToFullChat(),
|
|
||||||
);
|
|
||||||
|
|
||||||
// Global shortcut (Cmd+K / Ctrl+K)
|
// Global shortcut (Cmd+K / Ctrl+K)
|
||||||
document.addEventListener("keydown", (e) => {
|
document.addEventListener("keydown", (e) => {
|
||||||
|
|
@ -159,17 +147,11 @@ const Omnibox = {
|
||||||
|
|
||||||
if (e.key === "ArrowDown") {
|
if (e.key === "ArrowDown") {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
this.selectedIndex = Math.min(
|
this.selectedIndex = Math.min(this.selectedIndex + 1, actions.length - 1);
|
||||||
this.selectedIndex + 1,
|
|
||||||
actions.length - 1,
|
|
||||||
);
|
|
||||||
this.updateSelection(actions);
|
this.updateSelection(actions);
|
||||||
} else if (e.key === "ArrowUp") {
|
} else if (e.key === "ArrowUp") {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
this.selectedIndex = Math.max(
|
this.selectedIndex = Math.max(this.selectedIndex - 1, 0);
|
||||||
this.selectedIndex - 1,
|
|
||||||
0,
|
|
||||||
);
|
|
||||||
this.updateSelection(actions);
|
this.updateSelection(actions);
|
||||||
} else if (e.key === "Enter") {
|
} else if (e.key === "Enter") {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
@ -185,17 +167,13 @@ const Omnibox = {
|
||||||
|
|
||||||
updateSelection(actions) {
|
updateSelection(actions) {
|
||||||
actions.forEach((a, i) => {
|
actions.forEach((a, i) => {
|
||||||
a.classList.toggle(
|
a.classList.toggle("selected", i === this.selectedIndex);
|
||||||
"selected",
|
|
||||||
i === this.selectedIndex,
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
updateActions() {
|
updateActions() {
|
||||||
const currentApp = this.getCurrentApp();
|
const currentApp = this.getCurrentApp();
|
||||||
const actionsContainer =
|
const actionsContainer = document.getElementById("omniboxActions");
|
||||||
document.getElementById("omniboxActions");
|
|
||||||
|
|
||||||
const contextActions = {
|
const contextActions = {
|
||||||
chat: [
|
chat: [
|
||||||
|
|
@ -362,12 +340,8 @@ const Omnibox = {
|
||||||
.join("");
|
.join("");
|
||||||
|
|
||||||
// Rebind events
|
// Rebind events
|
||||||
actionsContainer
|
actionsContainer.querySelectorAll(".omnibox-action").forEach((btn) => {
|
||||||
.querySelectorAll(".omnibox-action")
|
btn.addEventListener("click", () => this.handleAction(btn));
|
||||||
.forEach((btn) => {
|
|
||||||
btn.addEventListener("click", () =>
|
|
||||||
this.handleAction(btn),
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
this.selectedIndex = 0;
|
this.selectedIndex = 0;
|
||||||
|
|
@ -398,9 +372,7 @@ const Omnibox = {
|
||||||
|
|
||||||
navigateTo(target) {
|
navigateTo(target) {
|
||||||
this.close();
|
this.close();
|
||||||
const link = document.querySelector(
|
const link = document.querySelector(`a[data-section="${target}"]`);
|
||||||
`a[data-section="${target}"]`,
|
|
||||||
);
|
|
||||||
if (link) {
|
if (link) {
|
||||||
link.click();
|
link.click();
|
||||||
}
|
}
|
||||||
|
|
@ -436,32 +408,24 @@ const Omnibox = {
|
||||||
},
|
},
|
||||||
|
|
||||||
showDefaultActions() {
|
showDefaultActions() {
|
||||||
document.getElementById(
|
document.getElementById("searchResultsSection").style.display = "none";
|
||||||
"searchResultsSection",
|
|
||||||
).style.display = "none";
|
|
||||||
this.updateActions();
|
this.updateActions();
|
||||||
},
|
},
|
||||||
|
|
||||||
searchContent(query) {
|
searchContent(query) {
|
||||||
// Show search results section
|
// Show search results section
|
||||||
const resultsSection = document.getElementById(
|
const resultsSection = document.getElementById("searchResultsSection");
|
||||||
"searchResultsSection",
|
const resultsList = document.getElementById("searchResultsList");
|
||||||
);
|
|
||||||
const resultsList =
|
|
||||||
document.getElementById("searchResultsList");
|
|
||||||
|
|
||||||
resultsSection.style.display = "block";
|
resultsSection.style.display = "block";
|
||||||
|
|
||||||
// Update first action to be "Ask about: query"
|
// Update first action to be "Ask about: query"
|
||||||
const actionsContainer =
|
const actionsContainer = document.getElementById("omniboxActions");
|
||||||
document.getElementById("omniboxActions");
|
const firstAction = actionsContainer.querySelector(".omnibox-action");
|
||||||
const firstAction =
|
|
||||||
actionsContainer.querySelector(".omnibox-action");
|
|
||||||
if (firstAction) {
|
if (firstAction) {
|
||||||
firstAction.dataset.action = "chat";
|
firstAction.dataset.action = "chat";
|
||||||
firstAction.dataset.query = query;
|
firstAction.dataset.query = query;
|
||||||
firstAction.querySelector(".action-icon").textContent =
|
firstAction.querySelector(".action-icon").textContent = "💬";
|
||||||
"💬";
|
|
||||||
firstAction.querySelector(".action-text").textContent =
|
firstAction.querySelector(".action-text").textContent =
|
||||||
`Ask: "${query.substring(0, 30)}${query.length > 30 ? "..." : ""}"`;
|
`Ask: "${query.substring(0, 30)}${query.length > 30 ? "..." : ""}"`;
|
||||||
}
|
}
|
||||||
|
|
@ -485,12 +449,8 @@ const Omnibox = {
|
||||||
'<div class="no-results">No results found. Try asking the bot!</div>';
|
'<div class="no-results">No results found. Try asking the bot!</div>';
|
||||||
|
|
||||||
// Bind click events
|
// Bind click events
|
||||||
resultsList
|
resultsList.querySelectorAll(".omnibox-result").forEach((btn) => {
|
||||||
.querySelectorAll(".omnibox-result")
|
btn.addEventListener("click", () => this.navigateTo(btn.dataset.target));
|
||||||
.forEach((btn) => {
|
|
||||||
btn.addEventListener("click", () =>
|
|
||||||
this.navigateTo(btn.dataset.target),
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
@ -581,17 +541,16 @@ const Omnibox = {
|
||||||
title: "Settings",
|
title: "Settings",
|
||||||
description: "App settings",
|
description: "App settings",
|
||||||
},
|
},
|
||||||
|
|
||||||
];
|
];
|
||||||
|
|
||||||
// Add plugin apps
|
// Add plugin apps
|
||||||
const pluginApps = window.Suite.getAllApps()
|
const pluginApps = window.Suite.getAllApps()
|
||||||
.filter(app => app.searchable)
|
.filter((app) => app.searchable)
|
||||||
.map(app => ({
|
.map((app) => ({
|
||||||
target: app.id,
|
target: app.id,
|
||||||
icon: app.icon || "📦",
|
icon: app.icon || "📦",
|
||||||
title: app.title || app.id,
|
title: app.title || app.id,
|
||||||
description: app.description || "App plugin"
|
description: app.description || "App plugin",
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const allItems = items.concat(pluginApps);
|
const allItems = items.concat(pluginApps);
|
||||||
|
|
@ -641,9 +600,7 @@ const Omnibox = {
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
this.addMessage(
|
this.addMessage(
|
||||||
data.reply ||
|
data.reply || data.message || "I received your message.",
|
||||||
data.message ||
|
|
||||||
"I received your message.",
|
|
||||||
"bot",
|
"bot",
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -660,10 +617,7 @@ const Omnibox = {
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.removeTypingIndicator(typingId);
|
this.removeTypingIndicator(typingId);
|
||||||
// Fallback response when API is not available
|
// Fallback response when API is not available
|
||||||
this.addMessage(
|
this.addMessage(this.getFallbackResponse(message), "bot");
|
||||||
this.getFallbackResponse(message),
|
|
||||||
"bot",
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
@ -697,8 +651,7 @@ const Omnibox = {
|
||||||
<div class="message-content">${this.escapeHtml(text)}</div>
|
<div class="message-content">${this.escapeHtml(text)}</div>
|
||||||
`;
|
`;
|
||||||
this.chatMessages.appendChild(msgDiv);
|
this.chatMessages.appendChild(msgDiv);
|
||||||
this.chatMessages.scrollTop =
|
this.chatMessages.scrollTop = this.chatMessages.scrollHeight;
|
||||||
this.chatMessages.scrollHeight;
|
|
||||||
|
|
||||||
this.chatHistory.push({ role: sender, content: text });
|
this.chatHistory.push({ role: sender, content: text });
|
||||||
},
|
},
|
||||||
|
|
@ -715,8 +668,7 @@ const Omnibox = {
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
this.chatMessages.appendChild(typingDiv);
|
this.chatMessages.appendChild(typingDiv);
|
||||||
this.chatMessages.scrollTop =
|
this.chatMessages.scrollTop = this.chatMessages.scrollHeight;
|
||||||
this.chatMessages.scrollHeight;
|
|
||||||
return id;
|
return id;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
@ -727,18 +679,13 @@ const Omnibox = {
|
||||||
|
|
||||||
handleBotAction(action) {
|
handleBotAction(action) {
|
||||||
if (action.navigate) {
|
if (action.navigate) {
|
||||||
setTimeout(
|
setTimeout(() => this.navigateTo(action.navigate), 1000);
|
||||||
() => this.navigateTo(action.navigate),
|
|
||||||
1000,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
expandToFullChat() {
|
expandToFullChat() {
|
||||||
this.close();
|
this.close();
|
||||||
const chatLink = document.querySelector(
|
const chatLink = document.querySelector('a[data-section="chat"]');
|
||||||
'a[data-section="chat"]',
|
|
||||||
);
|
|
||||||
if (chatLink) chatLink.click();
|
if (chatLink) chatLink.click();
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
@ -751,13 +698,57 @@ const Omnibox = {
|
||||||
|
|
||||||
// Initialize Omnibox when DOM is ready
|
// Initialize Omnibox when DOM is ready
|
||||||
document.addEventListener("DOMContentLoaded", () => {
|
document.addEventListener("DOMContentLoaded", () => {
|
||||||
|
// Detect bot name from pathname (e.g., /edu -> bot_name = "edu")
|
||||||
|
const detectBotFromPath = () => {
|
||||||
|
const pathname = window.location.pathname;
|
||||||
|
// Remove leading/trailing slashes and get first segment
|
||||||
|
const segments = pathname.replace(/^\/|\/$/g, "").split("/");
|
||||||
|
const firstSegment = segments[0];
|
||||||
|
|
||||||
|
// If first segment is not a known route, treat it as bot name
|
||||||
|
const knownRoutes = ["suite", "auth", "api", "static", "public"];
|
||||||
|
if (firstSegment && !knownRoutes.includes(firstSegment)) {
|
||||||
|
return firstSegment;
|
||||||
|
}
|
||||||
|
return "default";
|
||||||
|
};
|
||||||
|
|
||||||
|
// Set global bot name
|
||||||
|
window.__INITIAL_BOT_NAME__ = detectBotFromPath();
|
||||||
|
console.log(`🤖 Bot detected from path: ${window.__INITIAL_BOT_NAME__}`);
|
||||||
|
|
||||||
|
// Check if bot is public to skip authentication
|
||||||
|
const checkBotPublicStatus = async () => {
|
||||||
|
try {
|
||||||
|
const botName = window.__INITIAL_BOT_NAME__;
|
||||||
|
if (!botName || botName === "default") return;
|
||||||
|
|
||||||
|
const response = await fetch(
|
||||||
|
`/api/bot/config?bot_name=${encodeURIComponent(botName)}`,
|
||||||
|
);
|
||||||
|
if (response.ok) {
|
||||||
|
const config = await response.json();
|
||||||
|
if (config.public === true) {
|
||||||
|
window.__BOT_IS_PUBLIC__ = true;
|
||||||
|
console.log(
|
||||||
|
`✅ Bot '${botName}' is public - authentication not required`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.warn("Failed to check bot public status:", e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
Omnibox.init();
|
Omnibox.init();
|
||||||
console.log("🚀 Initializing General Bots with HTMX...");
|
console.log("🚀 Initializing General Bots with HTMX...");
|
||||||
|
|
||||||
|
// Check bot public status early
|
||||||
|
checkBotPublicStatus();
|
||||||
|
|
||||||
// Hide loading overlay
|
// Hide loading overlay
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
const loadingOverlay =
|
const loadingOverlay = document.getElementById("loadingOverlay");
|
||||||
document.getElementById("loadingOverlay");
|
|
||||||
if (loadingOverlay) {
|
if (loadingOverlay) {
|
||||||
loadingOverlay.classList.add("hidden");
|
loadingOverlay.classList.add("hidden");
|
||||||
}
|
}
|
||||||
|
|
@ -775,15 +766,11 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||||
const isOpen = appsDropdown.classList.toggle("show");
|
const isOpen = appsDropdown.classList.toggle("show");
|
||||||
appsBtn.setAttribute("aria-expanded", isOpen);
|
appsBtn.setAttribute("aria-expanded", isOpen);
|
||||||
// Close settings panel
|
// Close settings panel
|
||||||
if (settingsPanel)
|
if (settingsPanel) settingsPanel.classList.remove("show");
|
||||||
settingsPanel.classList.remove("show");
|
|
||||||
});
|
});
|
||||||
|
|
||||||
document.addEventListener("click", (e) => {
|
document.addEventListener("click", (e) => {
|
||||||
if (
|
if (!appsDropdown.contains(e.target) && !appsBtn.contains(e.target)) {
|
||||||
!appsDropdown.contains(e.target) &&
|
|
||||||
!appsBtn.contains(e.target)
|
|
||||||
) {
|
|
||||||
appsDropdown.classList.remove("show");
|
appsDropdown.classList.remove("show");
|
||||||
appsBtn.setAttribute("aria-expanded", "false");
|
appsBtn.setAttribute("aria-expanded", "false");
|
||||||
}
|
}
|
||||||
|
|
@ -813,8 +800,7 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||||
|
|
||||||
// Theme selection handling
|
// Theme selection handling
|
||||||
const themeOptions = document.querySelectorAll(".theme-option");
|
const themeOptions = document.querySelectorAll(".theme-option");
|
||||||
const savedTheme =
|
const savedTheme = localStorage.getItem("gb-theme") || "sentient";
|
||||||
localStorage.getItem("gb-theme") || "sentient";
|
|
||||||
|
|
||||||
// Apply saved theme
|
// Apply saved theme
|
||||||
document.body.setAttribute("data-theme", savedTheme);
|
document.body.setAttribute("data-theme", savedTheme);
|
||||||
|
|
@ -827,9 +813,7 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||||
const theme = option.getAttribute("data-theme");
|
const theme = option.getAttribute("data-theme");
|
||||||
document.body.setAttribute("data-theme", theme);
|
document.body.setAttribute("data-theme", theme);
|
||||||
localStorage.setItem("gb-theme", theme);
|
localStorage.setItem("gb-theme", theme);
|
||||||
themeOptions.forEach((o) =>
|
themeOptions.forEach((o) => o.classList.remove("active"));
|
||||||
o.classList.remove("active"),
|
|
||||||
);
|
|
||||||
option.classList.add("active");
|
option.classList.add("active");
|
||||||
|
|
||||||
// Update theme-color meta tag
|
// Update theme-color meta tag
|
||||||
|
|
@ -841,14 +825,9 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||||
orange: "#f97316",
|
orange: "#f97316",
|
||||||
sentient: "#d4f505",
|
sentient: "#d4f505",
|
||||||
};
|
};
|
||||||
const metaTheme = document.querySelector(
|
const metaTheme = document.querySelector('meta[name="theme-color"]');
|
||||||
'meta[name="theme-color"]',
|
|
||||||
);
|
|
||||||
if (metaTheme) {
|
if (metaTheme) {
|
||||||
metaTheme.setAttribute(
|
metaTheme.setAttribute("content", themeColors[theme] || "#d4f505");
|
||||||
"content",
|
|
||||||
themeColors[theme] || "#d4f505",
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
@ -959,17 +938,13 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||||
|
|
||||||
// Validate target exists before triggering HTMX load
|
// Validate target exists before triggering HTMX load
|
||||||
if (!mainContent) {
|
if (!mainContent) {
|
||||||
console.warn(
|
console.warn("handleHashChange: #main-content not found, skipping load");
|
||||||
"handleHashChange: #main-content not found, skipping load",
|
|
||||||
);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if main-content is in the DOM
|
// Check if main-content is in the DOM
|
||||||
if (!document.body.contains(mainContent)) {
|
if (!document.body.contains(mainContent)) {
|
||||||
console.warn(
|
console.warn("handleHashChange: #main-content not in DOM, skipping load");
|
||||||
"handleHashChange: #main-content not in DOM, skipping load",
|
|
||||||
);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -984,8 +959,7 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||||
// Debounce the load to prevent rapid double-requests
|
// Debounce the load to prevent rapid double-requests
|
||||||
pendingLoadTimeout = setTimeout(() => {
|
pendingLoadTimeout = setTimeout(() => {
|
||||||
// Re-check if section changed during debounce
|
// Re-check if section changed during debounce
|
||||||
const currentHash =
|
const currentHash = window.location.hash.slice(1) || "chat";
|
||||||
window.location.hash.slice(1) || "chat";
|
|
||||||
if (currentLoadedSection === currentHash) {
|
if (currentLoadedSection === currentHash) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -1005,10 +979,7 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||||
swap: "innerHTML",
|
swap: "innerHTML",
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn(
|
console.warn("handleHashChange: HTMX ajax error:", e);
|
||||||
"handleHashChange: HTMX ajax error:",
|
|
||||||
e,
|
|
||||||
);
|
|
||||||
currentLoadedSection = null;
|
currentLoadedSection = null;
|
||||||
isLoadingSection = false;
|
isLoadingSection = false;
|
||||||
}
|
}
|
||||||
|
|
@ -1019,10 +990,7 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||||
|
|
||||||
// Listen for HTMX swaps to track loaded sections and prevent duplicates
|
// Listen for HTMX swaps to track loaded sections and prevent duplicates
|
||||||
document.body.addEventListener("htmx:afterSwap", (event) => {
|
document.body.addEventListener("htmx:afterSwap", (event) => {
|
||||||
if (
|
if (event.detail.target && event.detail.target.id === "main-content") {
|
||||||
event.detail.target &&
|
|
||||||
event.detail.target.id === "main-content"
|
|
||||||
) {
|
|
||||||
const hash = window.location.hash.slice(1) || "chat";
|
const hash = window.location.hash.slice(1) || "chat";
|
||||||
currentLoadedSection = hash;
|
currentLoadedSection = hash;
|
||||||
isLoadingSection = false;
|
isLoadingSection = false;
|
||||||
|
|
@ -1031,27 +999,18 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||||
|
|
||||||
// Reset tracking on swap errors
|
// Reset tracking on swap errors
|
||||||
document.body.addEventListener("htmx:swapError", (event) => {
|
document.body.addEventListener("htmx:swapError", (event) => {
|
||||||
if (
|
if (event.detail.target && event.detail.target.id === "main-content") {
|
||||||
event.detail.target &&
|
|
||||||
event.detail.target.id === "main-content"
|
|
||||||
) {
|
|
||||||
isLoadingSection = false;
|
isLoadingSection = false;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Also listen for response errors
|
// Also listen for response errors
|
||||||
document.body.addEventListener(
|
document.body.addEventListener("htmx:responseError", (event) => {
|
||||||
"htmx:responseError",
|
if (event.detail.target && event.detail.target.id === "main-content") {
|
||||||
(event) => {
|
|
||||||
if (
|
|
||||||
event.detail.target &&
|
|
||||||
event.detail.target.id === "main-content"
|
|
||||||
) {
|
|
||||||
isLoadingSection = false;
|
isLoadingSection = false;
|
||||||
currentLoadedSection = null;
|
currentLoadedSection = null;
|
||||||
}
|
}
|
||||||
},
|
});
|
||||||
);
|
|
||||||
|
|
||||||
// Load initial content based on hash or default to chat
|
// Load initial content based on hash or default to chat
|
||||||
window.addEventListener("hashchange", handleHashChange);
|
window.addEventListener("hashchange", handleHashChange);
|
||||||
|
|
@ -1082,17 +1041,13 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||||
const list = document.getElementById("notificationsList");
|
const list = document.getElementById("notificationsList");
|
||||||
const btn = document.getElementById("notificationsBtn");
|
const btn = document.getElementById("notificationsBtn");
|
||||||
const panel = document.getElementById("notificationsPanel");
|
const panel = document.getElementById("notificationsPanel");
|
||||||
const clearBtn = document.getElementById(
|
const clearBtn = document.getElementById("clearNotificationsBtn");
|
||||||
"clearNotificationsBtn",
|
|
||||||
);
|
|
||||||
|
|
||||||
function updateBadge() {
|
function updateBadge() {
|
||||||
if (badge) {
|
if (badge) {
|
||||||
if (notifications.length > 0) {
|
if (notifications.length > 0) {
|
||||||
badge.textContent =
|
badge.textContent =
|
||||||
notifications.length > 99
|
notifications.length > 99 ? "99+" : notifications.length;
|
||||||
? "99+"
|
|
||||||
: notifications.length;
|
|
||||||
badge.style.display = "flex";
|
badge.style.display = "flex";
|
||||||
} else {
|
} else {
|
||||||
badge.style.display = "none";
|
badge.style.display = "none";
|
||||||
|
|
@ -1172,10 +1127,7 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
document.addEventListener("click", (e) => {
|
document.addEventListener("click", (e) => {
|
||||||
if (
|
if (!panel.contains(e.target) && !btn.contains(e.target)) {
|
||||||
!panel.contains(e.target) &&
|
|
||||||
!btn.contains(e.target)
|
|
||||||
) {
|
|
||||||
panel.classList.remove("show");
|
panel.classList.remove("show");
|
||||||
btn.setAttribute("aria-expanded", "false");
|
btn.setAttribute("aria-expanded", "false");
|
||||||
}
|
}
|
||||||
|
|
@ -1241,9 +1193,7 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||||
? "🔴"
|
? "🔴"
|
||||||
: "🟡",
|
: "🟡",
|
||||||
title:
|
title:
|
||||||
"Connection " +
|
"Connection " + status.charAt(0).toUpperCase() + status.slice(1),
|
||||||
status.charAt(0).toUpperCase() +
|
|
||||||
status.slice(1),
|
|
||||||
message: message || "",
|
message: message || "",
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
@ -1256,8 +1206,7 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||||
if (e.altKey && !e.ctrlKey && !e.shiftKey) {
|
if (e.altKey && !e.ctrlKey && !e.shiftKey) {
|
||||||
const num = parseInt(e.key);
|
const num = parseInt(e.key);
|
||||||
if (num >= 1 && num <= 9) {
|
if (num >= 1 && num <= 9) {
|
||||||
const items =
|
const items = document.querySelectorAll(".app-item");
|
||||||
document.querySelectorAll(".app-item");
|
|
||||||
if (items[num - 1]) {
|
if (items[num - 1]) {
|
||||||
items[num - 1].click();
|
items[num - 1].click();
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
@ -1281,21 +1230,18 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||||
const userName = document.getElementById("userName");
|
const userName = document.getElementById("userName");
|
||||||
const userEmail = document.getElementById("userEmail");
|
const userEmail = document.getElementById("userEmail");
|
||||||
const userAvatar = document.getElementById("userAvatar");
|
const userAvatar = document.getElementById("userAvatar");
|
||||||
const userAvatarLarge =
|
const userAvatarLarge = document.getElementById("userAvatarLarge");
|
||||||
document.getElementById("userAvatarLarge");
|
|
||||||
const authAction = document.getElementById("authAction");
|
const authAction = document.getElementById("authAction");
|
||||||
const authText = document.getElementById("authText");
|
const authText = document.getElementById("authText");
|
||||||
const authIcon = document.getElementById("authIcon");
|
const authIcon = document.getElementById("authIcon");
|
||||||
|
const settingsBtn = document.getElementById("settingsBtn");
|
||||||
|
const appsButton = document.getElementById("appsButton");
|
||||||
|
const notificationsBtn = document.getElementById("notificationsBtn");
|
||||||
|
|
||||||
const displayName =
|
const displayName =
|
||||||
user.display_name ||
|
user.display_name || user.first_name || user.username || "User";
|
||||||
user.first_name ||
|
|
||||||
user.username ||
|
|
||||||
"User";
|
|
||||||
const email = user.email || "";
|
const email = user.email || "";
|
||||||
const initial = (
|
const initial = (displayName.charAt(0) || "U").toUpperCase();
|
||||||
displayName.charAt(0) || "U"
|
|
||||||
).toUpperCase();
|
|
||||||
|
|
||||||
console.log("Updating user UI:", displayName, email);
|
console.log("Updating user UI:", displayName, email);
|
||||||
|
|
||||||
|
|
@ -1328,14 +1274,20 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||||
authIcon.innerHTML =
|
authIcon.innerHTML =
|
||||||
'<path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"></path><polyline points="16 17 21 12 16 7"></polyline><line x1="21" y1="12" x2="9" y2="12"></line>';
|
'<path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"></path><polyline points="16 17 21 12 16 7"></polyline><line x1="21" y1="12" x2="9" y2="12"></line>';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (settingsBtn) settingsBtn.style.display = "";
|
||||||
|
if (appsButton) appsButton.style.display = "";
|
||||||
|
if (notificationsBtn) notificationsBtn.style.display = "";
|
||||||
}
|
}
|
||||||
|
|
||||||
function loadUserProfile() {
|
function loadUserProfile() {
|
||||||
var token =
|
var token =
|
||||||
localStorage.getItem("gb-access-token") ||
|
localStorage.getItem("gb-access-token") ||
|
||||||
sessionStorage.getItem("gb-access-token");
|
sessionStorage.getItem("gb-access-token");
|
||||||
|
|
||||||
if (!token) {
|
if (!token) {
|
||||||
console.log("No auth token found");
|
console.log("No auth token found - user is signed out");
|
||||||
|
updateSignedOutUI();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1348,22 +1300,60 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||||
headers: { Authorization: "Bearer " + token },
|
headers: { Authorization: "Bearer " + token },
|
||||||
})
|
})
|
||||||
.then(function (res) {
|
.then(function (res) {
|
||||||
if (!res.ok) throw new Error("Not authenticated");
|
if (!res.ok) {
|
||||||
|
console.log("User not authenticated");
|
||||||
|
updateSignedOutUI();
|
||||||
|
throw new Error("Not authenticated");
|
||||||
|
}
|
||||||
return res.json();
|
return res.json();
|
||||||
})
|
})
|
||||||
.then(function (user) {
|
.then(function (user) {
|
||||||
console.log("User profile loaded:", user);
|
console.log("User profile loaded:", user);
|
||||||
updateUserUI(user);
|
updateUserUI(user);
|
||||||
localStorage.setItem(
|
localStorage.setItem("gb-user-data", JSON.stringify(user));
|
||||||
"gb-user-data",
|
|
||||||
JSON.stringify(user),
|
|
||||||
);
|
|
||||||
})
|
})
|
||||||
.catch(function (err) {
|
.catch(function (err) {
|
||||||
console.log("Failed to load user profile:", err);
|
console.log("Failed to load user profile:", err);
|
||||||
|
updateSignedOutUI();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function updateSignedOutUI() {
|
||||||
|
const userName = document.getElementById("userName");
|
||||||
|
const userEmail = document.getElementById("userEmail");
|
||||||
|
const userAvatar = document.getElementById("userAvatar");
|
||||||
|
const userAvatarLarge = document.getElementById("userAvatarLarge");
|
||||||
|
const authAction = document.getElementById("authAction");
|
||||||
|
const authText = document.getElementById("authText");
|
||||||
|
const authIcon = document.getElementById("authIcon");
|
||||||
|
const settingsBtn = document.getElementById("settingsBtn");
|
||||||
|
const appsButton = document.getElementById("appsButton");
|
||||||
|
const notificationsBtn = document.getElementById("notificationsBtn");
|
||||||
|
|
||||||
|
if (userName) userName.textContent = "User";
|
||||||
|
if (userEmail) userEmail.textContent = "user@example.com";
|
||||||
|
if (userAvatar) {
|
||||||
|
const avatarSpan = userAvatar.querySelector("span");
|
||||||
|
if (avatarSpan) avatarSpan.textContent = "U";
|
||||||
|
}
|
||||||
|
if (userAvatarLarge) userAvatarLarge.textContent = "U";
|
||||||
|
|
||||||
|
if (authAction) {
|
||||||
|
authAction.href = "/auth/login.html";
|
||||||
|
authAction.removeAttribute("onclick");
|
||||||
|
authAction.style.color = "var(--primary)";
|
||||||
|
}
|
||||||
|
if (authText) authText.textContent = "Sign in";
|
||||||
|
if (authIcon) {
|
||||||
|
authIcon.innerHTML =
|
||||||
|
'<path d="M15 3h4a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2h-4"></path><polyline points="10 17 15 12 21 12"></polyline><line x1="15" y1="12" x2="3" y2="12"></line>';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (settingsBtn) settingsBtn.style.display = "none";
|
||||||
|
if (appsButton) appsButton.style.display = "none";
|
||||||
|
if (notificationsBtn) notificationsBtn.style.display = "none";
|
||||||
|
}
|
||||||
|
|
||||||
// Try to load cached user first
|
// Try to load cached user first
|
||||||
var cachedUser = localStorage.getItem("gb-user-data");
|
var cachedUser = localStorage.getItem("gb-user-data");
|
||||||
if (cachedUser) {
|
if (cachedUser) {
|
||||||
|
|
@ -1372,15 +1362,12 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||||
if (user && user.email) {
|
if (user && user.email) {
|
||||||
updateUserUI(user);
|
updateUserUI(user);
|
||||||
}
|
}
|
||||||
} catch (e) { }
|
} catch (e) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Always fetch fresh user data
|
// Always fetch fresh user data
|
||||||
if (document.readyState === "loading") {
|
if (document.readyState === "loading") {
|
||||||
document.addEventListener(
|
document.addEventListener("DOMContentLoaded", loadUserProfile);
|
||||||
"DOMContentLoaded",
|
|
||||||
loadUserProfile,
|
|
||||||
);
|
|
||||||
} else {
|
} else {
|
||||||
loadUserProfile();
|
loadUserProfile();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
<link rel="stylesheet" href="tasks/autotask.css" />
|
<link rel="stylesheet" href="/suite/tasks/autotask.css" />
|
||||||
|
|
||||||
<div class="autotask-container" data-theme="sentient">
|
<div class="autotask-container" data-theme="sentient">
|
||||||
<!-- Top Navigation Bar -->
|
<!-- Top Navigation Bar -->
|
||||||
|
|
@ -478,6 +478,6 @@ Examples:
|
||||||
<!-- Toast Container -->
|
<!-- Toast Container -->
|
||||||
<div class="toast-container" id="toast-container"></div>
|
<div class="toast-container" id="toast-container"></div>
|
||||||
|
|
||||||
<link rel="stylesheet" href="tasks/progress-panel.css" />
|
<link rel="stylesheet" href="/suite/tasks/progress-panel.css" />
|
||||||
<script src="tasks/progress-panel.js"></script>
|
<script src="/suite/tasks/progress-panel.js"></script>
|
||||||
<script src="tasks/autotask.js"></script>
|
<script src="/suite/tasks/autotask.js"></script>
|
||||||
|
|
|
||||||
|
|
@ -213,8 +213,10 @@ function setupIntentInputHandlers() {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Task polling for async task creation
|
// Task polling for async task creation
|
||||||
let activePollingTaskId = null;
|
if (typeof activePollingTaskId === "undefined") {
|
||||||
let pollingInterval = null;
|
var activePollingTaskId = null;
|
||||||
|
var pollingInterval = null;
|
||||||
|
}
|
||||||
|
|
||||||
function startTaskPolling(taskId) {
|
function startTaskPolling(taskId) {
|
||||||
// Stop any existing polling
|
// Stop any existing polling
|
||||||
|
|
@ -629,7 +631,9 @@ function handleWebSocketMessage(data) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Store pending manifest updates for tasks whose elements aren't loaded yet
|
// Store pending manifest updates for tasks whose elements aren't loaded yet
|
||||||
const pendingManifestUpdates = new Map();
|
if (typeof pendingManifestUpdates === "undefined") {
|
||||||
|
var pendingManifestUpdates = new Map();
|
||||||
|
}
|
||||||
|
|
||||||
function renderManifestProgress(
|
function renderManifestProgress(
|
||||||
taskId,
|
taskId,
|
||||||
|
|
@ -2759,8 +2763,9 @@ function formatTime(seconds) {
|
||||||
// GLOBAL STYLES FOR TOAST ANIMATIONS
|
// GLOBAL STYLES FOR TOAST ANIMATIONS
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
|
|
||||||
const style = document.createElement("style");
|
if (typeof taskStyleElement === "undefined") {
|
||||||
style.textContent = `
|
var taskStyleElement = document.createElement("style");
|
||||||
|
taskStyleElement.textContent = `
|
||||||
@keyframes slideIn {
|
@keyframes slideIn {
|
||||||
from {
|
from {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
|
|
@ -2805,7 +2810,8 @@ style.textContent = `
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
document.head.appendChild(style);
|
document.head.appendChild(taskStyleElement);
|
||||||
|
}
|
||||||
|
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
// GOALS, PENDING INFO, SCHEDULERS, MONITORS
|
// GOALS, PENDING INFO, SCHEDULERS, MONITORS
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue