Compare commits
6 commits
661edc09fa
...
af78f31565
| Author | SHA1 | Date | |
|---|---|---|---|
| af78f31565 | |||
| a8bc5530b0 | |||
| 123787378f | |||
|
|
1bf9510c7d | ||
|
|
6b1dcc9d3f | ||
|
|
607ad076d9 |
17 changed files with 1087 additions and 165 deletions
6
build.rs
6
build.rs
|
|
@ -1,3 +1,5 @@
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
}
|
let manifest_dir = std::env::var("CARGO_MANIFEST_DIR").unwrap();
|
||||||
|
let ui_path = std::path::Path::new(&manifest_dir).join("ui");
|
||||||
|
println!("cargo:rustc-env=BOTUI_UI_PATH={}", ui_path.display());
|
||||||
|
}
|
||||||
|
|
@ -25,7 +25,7 @@ use tower_http::services::{ServeDir, ServeFile};
|
||||||
|
|
||||||
#[cfg(feature = "embed-ui")]
|
#[cfg(feature = "embed-ui")]
|
||||||
#[derive(RustEmbed)]
|
#[derive(RustEmbed)]
|
||||||
#[folder = "$CARGO_MANIFEST_DIR/ui"]
|
#[folder = "ui"]
|
||||||
struct Assets;
|
struct Assets;
|
||||||
|
|
||||||
use crate::shared::AppState;
|
use crate::shared::AppState;
|
||||||
|
|
@ -163,15 +163,19 @@ pub async fn index(OriginalUri(uri): OriginalUri) -> Response {
|
||||||
let fs_path = if path_parts.len() > 1 {
|
let fs_path = if path_parts.len() > 1 {
|
||||||
let mut start_idx = 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"];
|
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"];
|
||||||
|
|
||||||
|
// Special case: /auth/suite/* should map to suite/* (auth is a route, not a directory)
|
||||||
|
if path_parts.get(1) == Some(&"auth") && path_parts.get(2) == Some(&"suite") {
|
||||||
|
start_idx = 2;
|
||||||
|
}
|
||||||
// Skip bot name if present (first segment is not a known dir, second segment is)
|
// Skip bot name if present (first segment is not a known dir, second segment is)
|
||||||
if path_parts.len() > start_idx + 1
|
else if path_parts.len() > start_idx + 1
|
||||||
&& !known_dirs.contains(&path_parts[start_idx])
|
&& !known_dirs.contains(&path_parts[start_idx])
|
||||||
&& known_dirs.contains(&path_parts[start_idx + 1])
|
&& known_dirs.contains(&path_parts[start_idx + 1])
|
||||||
{
|
{
|
||||||
start_idx += 1;
|
start_idx += 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
path_parts[start_idx..].join("/")
|
path_parts[start_idx..].join("/")
|
||||||
} else {
|
} else {
|
||||||
path.to_string()
|
path.to_string()
|
||||||
|
|
@ -338,25 +342,39 @@ pub async fn serve_suite(bot_name: Option<String>) -> impl IntoResponse {
|
||||||
|
|
||||||
// Inject base tag and bot_name into the page
|
// Inject base tag and bot_name into the page
|
||||||
if let Some(head_end) = html.find("</head>") {
|
if let Some(head_end) = html.find("</head>") {
|
||||||
|
// Check if bot_name is actually an auth page (login.html, register.html, etc.)
|
||||||
|
// These are not actual bots, so we should use "/" as base href
|
||||||
|
let is_auth_page = bot_name.as_ref()
|
||||||
|
.map(|n| n.ends_with(".html") || n == "login" || n == "register" || n == "forgot-password" || n == "reset-password")
|
||||||
|
.unwrap_or(false);
|
||||||
|
|
||||||
// Set base href to include bot context if present (e.g., /edu/)
|
// Set base href to include bot context if present (e.g., /edu/)
|
||||||
let base_href = if let Some(ref name) = bot_name {
|
// But NOT for auth pages - those use root
|
||||||
|
let base_href = if is_auth_page {
|
||||||
|
"/".to_string()
|
||||||
|
} else if let Some(ref name) = bot_name {
|
||||||
format!("/{}/", name)
|
format!("/{}/", name)
|
||||||
} else {
|
} else {
|
||||||
"/".to_string()
|
"/".to_string()
|
||||||
};
|
};
|
||||||
let base_tag = format!(r#"<base href="{}">"#, base_href);
|
let base_tag = format!(r#"<base href="{}">"#, base_href);
|
||||||
html.insert_str(head_end, &base_tag);
|
html.insert_str(head_end, &base_tag);
|
||||||
|
|
||||||
if let Some(name) = bot_name {
|
// Only inject bot_name script for actual bots, not auth pages
|
||||||
info!("serve_suite: Injecting bot_name '{}' into page with base href='{}'", name, base_href);
|
if !is_auth_page {
|
||||||
let bot_script = format!(
|
if let Some(name) = bot_name {
|
||||||
r#"<script>window.__INITIAL_BOT_NAME__ = "{}";</script>"#,
|
info!("serve_suite: Injecting bot_name '{}' into page with base href='{}'", name, base_href);
|
||||||
&name
|
let bot_script = format!(
|
||||||
);
|
r#"<script>window.__INITIAL_BOT_NAME__ = "{}";</script>"#,
|
||||||
html.insert_str(head_end + base_tag.len(), &bot_script);
|
&name
|
||||||
info!("serve_suite: Successfully injected base tag and bot_name script");
|
);
|
||||||
|
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 {
|
} else {
|
||||||
info!("serve_suite: Successfully injected base tag (no bot_name)");
|
info!("serve_suite: Auth page detected, skipping bot_name injection (base href='{}')", base_href);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
error!("serve_suite: Failed to find </head> tag to inject content");
|
error!("serve_suite: Failed to find </head> tag to inject content");
|
||||||
|
|
@ -1150,6 +1168,23 @@ async fn handle_embedded_root_asset(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "embed-ui")]
|
||||||
|
async fn handle_auth_asset(axum::extract::Path(path): axum::extract::Path<String>) -> impl IntoResponse {
|
||||||
|
let normalized_path = path.strip_prefix('/').unwrap_or(&path);
|
||||||
|
let asset_path = format!("suite/auth/{}", normalized_path);
|
||||||
|
match Assets::get(&asset_path) {
|
||||||
|
Some(content) => {
|
||||||
|
let mime = mime_guess::from_path(&asset_path).first_or_octet_stream();
|
||||||
|
(
|
||||||
|
[(axum::http::header::CONTENT_TYPE, mime.as_ref())],
|
||||||
|
content.data,
|
||||||
|
)
|
||||||
|
.into_response()
|
||||||
|
}
|
||||||
|
None => StatusCode::NOT_FOUND.into_response(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn add_static_routes(router: Router<AppState>, _suite_path: &Path) -> Router<AppState> {
|
fn add_static_routes(router: Router<AppState>, _suite_path: &Path) -> Router<AppState> {
|
||||||
#[cfg(feature = "embed-ui")]
|
#[cfg(feature = "embed-ui")]
|
||||||
{
|
{
|
||||||
|
|
@ -1195,6 +1230,16 @@ pub fn configure_router() -> Router {
|
||||||
.route("/minimal", get(serve_minimal))
|
.route("/minimal", get(serve_minimal))
|
||||||
.route("/suite", get(serve_suite));
|
.route("/suite", get(serve_suite));
|
||||||
|
|
||||||
|
#[cfg(not(feature = "embed-ui"))]
|
||||||
|
{
|
||||||
|
router = router.nest_service("/auth", ServeDir::new(suite_path.join("auth")));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "embed-ui")]
|
||||||
|
{
|
||||||
|
router = router.route("/auth/*path", get(handle_auth_asset));
|
||||||
|
}
|
||||||
|
|
||||||
router = add_static_routes(router, &suite_path);
|
router = add_static_routes(router, &suite_path);
|
||||||
|
|
||||||
router.fallback(get(index)).with_state(state)
|
router.fallback(get(index)).with_state(state)
|
||||||
|
|
|
||||||
|
|
@ -153,7 +153,7 @@
|
||||||
<script>
|
<script>
|
||||||
// Configuration
|
// Configuration
|
||||||
const CONFIG = {
|
const CONFIG = {
|
||||||
serverUrl: window.BOTSERVER_URL || 'http://localhost:8088',
|
serverUrl: window.BOTSERVER_URL || 'http://localhost:9000',
|
||||||
maxMessages: 10, // Keep memory low
|
maxMessages: 10, // Keep memory low
|
||||||
maxMsgLen: 100, // Truncate long messages
|
maxMsgLen: 100, // Truncate long messages
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -5,9 +5,9 @@
|
||||||
<title>General Bots</title>
|
<title>General Bots</title>
|
||||||
<meta name="viewport" content="width=device-width,initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width,initial-scale=1.0" />
|
||||||
<script>
|
<script>
|
||||||
// BotServer URL - configurable via window.BOTSERVER_URL or defaults to same origin port 8088
|
// BotServer URL - configurable via window.BOTSERVER_URL or defaults to same origin port 9000
|
||||||
const BOTSERVER_URL =
|
const BOTSERVER_URL =
|
||||||
window.BOTSERVER_URL || "https://localhost:8088";
|
window.BOTSERVER_URL || "https://localhost:9000";
|
||||||
const BOTSERVER_WS_URL = BOTSERVER_URL.replace(
|
const BOTSERVER_WS_URL = BOTSERVER_URL.replace(
|
||||||
"https://",
|
"https://",
|
||||||
"wss://",
|
"wss://",
|
||||||
|
|
|
||||||
73
ui/public/themes/dark.css
Normal file
73
ui/public/themes/dark.css
Normal file
|
|
@ -0,0 +1,73 @@
|
||||||
|
/* Dark Theme for General Bots */
|
||||||
|
:root {
|
||||||
|
--color-primary: #d4f505;
|
||||||
|
--color-secondary: #00d4aa;
|
||||||
|
--color-accent: #818cf8;
|
||||||
|
|
||||||
|
--color-bg: #0f172a;
|
||||||
|
--color-bg-secondary: #1e293b;
|
||||||
|
--color-bg-tertiary: #334155;
|
||||||
|
|
||||||
|
--color-text: #f1f5f9;
|
||||||
|
--color-text-secondary: #cbd5e1;
|
||||||
|
--color-text-muted: #64748b;
|
||||||
|
|
||||||
|
--color-border: #334155;
|
||||||
|
--color-border-light: #1e293b;
|
||||||
|
|
||||||
|
--color-success: #22c55e;
|
||||||
|
--color-warning: #f59e0b;
|
||||||
|
--color-error: #ef4444;
|
||||||
|
--color-info: #3b82f6;
|
||||||
|
|
||||||
|
--shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 0.3);
|
||||||
|
--shadow: 0 1px 3px 0 rgb(0 0 0 / 0.4), 0 1px 2px -1px rgb(0 0 0 / 0.4);
|
||||||
|
--shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.4), 0 2px 4px -2px rgb(0 0 0 / 0.4);
|
||||||
|
--shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.5), 0 4px 6px -4px rgb(0 0 0 / 0.5);
|
||||||
|
|
||||||
|
--radius-sm: 0.25rem;
|
||||||
|
--radius: 0.375rem;
|
||||||
|
--radius-md: 0.5rem;
|
||||||
|
--radius-lg: 0.75rem;
|
||||||
|
--radius-xl: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
background-color: var(--color-bg);
|
||||||
|
color: var(--color-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: var(--color-accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
a:hover {
|
||||||
|
color: var(--color-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary {
|
||||||
|
background-color: var(--color-primary);
|
||||||
|
color: var(--color-bg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary:hover {
|
||||||
|
background-color: var(--color-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
background-color: var(--color-bg-secondary);
|
||||||
|
border: 1px solid var(--color-border);
|
||||||
|
box-shadow: var(--shadow-sm);
|
||||||
|
}
|
||||||
|
|
||||||
|
input, textarea, select {
|
||||||
|
background-color: var(--color-bg-secondary);
|
||||||
|
border: 1px solid var(--color-border);
|
||||||
|
color: var(--color-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
input:focus, textarea:focus, select:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: var(--color-primary);
|
||||||
|
box-shadow: 0 0 0 3px rgba(212, 245, 5, 0.1);
|
||||||
|
}
|
||||||
73
ui/public/themes/light.css
Normal file
73
ui/public/themes/light.css
Normal file
|
|
@ -0,0 +1,73 @@
|
||||||
|
/* Light Theme for General Bots */
|
||||||
|
:root {
|
||||||
|
--color-primary: #d4f505;
|
||||||
|
--color-secondary: #00d4aa;
|
||||||
|
--color-accent: #6366f1;
|
||||||
|
|
||||||
|
--color-bg: #ffffff;
|
||||||
|
--color-bg-secondary: #f8fafc;
|
||||||
|
--color-bg-tertiary: #f1f5f9;
|
||||||
|
|
||||||
|
--color-text: #0f172a;
|
||||||
|
--color-text-secondary: #475569;
|
||||||
|
--color-text-muted: #94a3b8;
|
||||||
|
|
||||||
|
--color-border: #e2e8f0;
|
||||||
|
--color-border-light: #f1f5f9;
|
||||||
|
|
||||||
|
--color-success: #22c55e;
|
||||||
|
--color-warning: #f59e0b;
|
||||||
|
--color-error: #ef4444;
|
||||||
|
--color-info: #3b82f6;
|
||||||
|
|
||||||
|
--shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 0.05);
|
||||||
|
--shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1);
|
||||||
|
--shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1);
|
||||||
|
--shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1);
|
||||||
|
|
||||||
|
--radius-sm: 0.25rem;
|
||||||
|
--radius: 0.375rem;
|
||||||
|
--radius-md: 0.5rem;
|
||||||
|
--radius-lg: 0.75rem;
|
||||||
|
--radius-xl: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
background-color: var(--color-bg);
|
||||||
|
color: var(--color-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: var(--color-accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
a:hover {
|
||||||
|
color: var(--color-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary {
|
||||||
|
background-color: var(--color-primary);
|
||||||
|
color: var(--color-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary:hover {
|
||||||
|
background-color: var(--color-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
background-color: var(--color-bg);
|
||||||
|
border: 1px solid var(--color-border);
|
||||||
|
box-shadow: var(--shadow-sm);
|
||||||
|
}
|
||||||
|
|
||||||
|
input, textarea, select {
|
||||||
|
background-color: var(--color-bg);
|
||||||
|
border: 1px solid var(--color-border);
|
||||||
|
color: var(--color-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
input:focus, textarea:focus, select:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: var(--color-primary);
|
||||||
|
box-shadow: 0 0 0 3px rgba(212, 245, 5, 0.1);
|
||||||
|
}
|
||||||
117
ui/public/themes/y2kglow.css
Normal file
117
ui/public/themes/y2kglow.css
Normal file
|
|
@ -0,0 +1,117 @@
|
||||||
|
/* Y2K Glow Theme for General Bots */
|
||||||
|
:root {
|
||||||
|
--color-primary: #ff00ff;
|
||||||
|
--color-secondary: #00ffff;
|
||||||
|
--color-accent: #ffff00;
|
||||||
|
|
||||||
|
--color-bg: #0a0a1a;
|
||||||
|
--color-bg-secondary: #1a0a2e;
|
||||||
|
--color-bg-tertiary: #2d1b4e;
|
||||||
|
|
||||||
|
--color-text: #00ff00;
|
||||||
|
--color-text-secondary: #ff00ff;
|
||||||
|
--color-text-muted: #00ffff;
|
||||||
|
|
||||||
|
--color-border: #ff00ff;
|
||||||
|
--color-border-light: #00ffff;
|
||||||
|
|
||||||
|
--color-success: #00ff00;
|
||||||
|
--color-warning: #ffff00;
|
||||||
|
--color-error: #ff0066;
|
||||||
|
--color-info: #00ffff;
|
||||||
|
|
||||||
|
--shadow-glow: 0 0 10px #ff00ff, 0 0 20px #ff00ff, 0 0 30px #ff00ff;
|
||||||
|
--shadow-sm: 0 0 5px rgba(255, 0, 255, 0.5);
|
||||||
|
--shadow: 0 0 10px rgba(255, 0, 255, 0.7);
|
||||||
|
--shadow-md: 0 0 15px rgba(255, 0, 255, 0.8);
|
||||||
|
--shadow-lg: 0 0 25px rgba(255, 0, 255, 0.9);
|
||||||
|
|
||||||
|
--radius-sm: 0.25rem;
|
||||||
|
--radius: 0.375rem;
|
||||||
|
--radius-md: 0.5rem;
|
||||||
|
--radius-lg: 0.75rem;
|
||||||
|
--radius-xl: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
background-color: var(--color-bg);
|
||||||
|
color: var(--color-text);
|
||||||
|
text-shadow: 0 0 5px var(--color-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: var(--color-secondary);
|
||||||
|
text-shadow: 0 0 5px var(--color-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
a:hover {
|
||||||
|
color: var(--color-primary);
|
||||||
|
text-shadow: 0 0 10px var(--color-primary), 0 0 20px var(--color-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary {
|
||||||
|
background: linear-gradient(45deg, var(--color-primary), var(--color-secondary));
|
||||||
|
color: var(--color-bg);
|
||||||
|
border: 2px solid var(--color-primary);
|
||||||
|
box-shadow: var(--shadow-glow);
|
||||||
|
text-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary:hover {
|
||||||
|
background: linear-gradient(45deg, var(--color-secondary), var(--color-accent));
|
||||||
|
border-color: var(--color-secondary);
|
||||||
|
box-shadow: 0 0 15px var(--color-secondary), 0 0 30px var(--color-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
background: linear-gradient(135deg, var(--color-bg-secondary), var(--color-bg-tertiary));
|
||||||
|
border: 2px solid var(--color-primary);
|
||||||
|
box-shadow: var(--shadow);
|
||||||
|
animation: glow 2s ease-in-out infinite alternate;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes glow {
|
||||||
|
from {
|
||||||
|
box-shadow: 0 0 5px var(--color-primary), 0 0 10px var(--color-primary);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
box-shadow: 0 0 10px var(--color-secondary), 0 0 20px var(--color-secondary);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
input, textarea, select {
|
||||||
|
background: var(--color-bg-secondary);
|
||||||
|
border: 2px solid var(--color-border);
|
||||||
|
color: var(--color-text);
|
||||||
|
box-shadow: 0 0 5px var(--color-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
input:focus, textarea:focus, select:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: var(--color-accent);
|
||||||
|
box-shadow: 0 0 10px var(--color-accent), 0 0 20px var(--color-accent), 0 0 30px var(--color-accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
input::placeholder, textarea::placeholder {
|
||||||
|
color: var(--color-text-muted);
|
||||||
|
text-shadow: 0 0 3px var(--color-text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar {
|
||||||
|
width: 12px;
|
||||||
|
height: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-track {
|
||||||
|
background: var(--color-bg);
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-thumb {
|
||||||
|
background: linear-gradient(var(--color-primary), var(--color-secondary));
|
||||||
|
border-radius: 6px;
|
||||||
|
box-shadow: 0 0 10px var(--color-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: linear-gradient(var(--color-secondary), var(--color-accent));
|
||||||
|
}
|
||||||
|
|
@ -21,6 +21,8 @@
|
||||||
<link rel="stylesheet" href="/css/app.css" />
|
<link rel="stylesheet" href="/css/app.css" />
|
||||||
<link rel="stylesheet" href="/themes/sentient/sentient.css" />
|
<link rel="stylesheet" href="/themes/sentient/sentient.css" />
|
||||||
<link rel="stylesheet" href="/css/ai-panel.css" />
|
<link rel="stylesheet" href="/css/ai-panel.css" />
|
||||||
|
<!-- Config color overrides - must load AFTER theme CSS -->
|
||||||
|
<link rel="stylesheet" href="/css/config-colors.css" />
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<!-- Skip navigation link for accessibility -->
|
<!-- Skip navigation link for accessibility -->
|
||||||
|
|
|
||||||
|
|
@ -112,32 +112,34 @@
|
||||||
#messages {
|
#messages {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
|
overflow-x: hidden;
|
||||||
padding: 20px 0;
|
padding: 20px 0;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 16px;
|
gap: 16px;
|
||||||
scrollbar-width: thin;
|
scrollbar-width: thin;
|
||||||
scrollbar-color: var(--accent, #3b82f6) var(--surface, #1a1a24);
|
scrollbar-color: var(--accent, #3b82f6) var(--surface, #1a1a24);
|
||||||
|
scroll-behavior: smooth;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Custom scrollbar for markers */
|
/* Enhanced custom scrollbar */
|
||||||
#messages::-webkit-scrollbar {
|
#messages::-webkit-scrollbar {
|
||||||
width: 6px;
|
width: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
#messages::-webkit-scrollbar-track {
|
#messages::-webkit-scrollbar-track {
|
||||||
background: var(--surface, #1a1a24);
|
background: transparent;
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
}
|
}
|
||||||
|
|
||||||
#messages::-webkit-scrollbar-thumb {
|
#messages::-webkit-scrollbar-thumb {
|
||||||
background: var(--accent, #3b82f6);
|
background: var(--border, rgba(255, 255, 255, 0.2));
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
border: 1px solid var(--surface, #1a1a24);
|
transition: background 0.2s;
|
||||||
}
|
}
|
||||||
|
|
||||||
#messages::-webkit-scrollbar-thumb:hover {
|
#messages::-webkit-scrollbar-thumb:hover {
|
||||||
background: var(--accent-hover, #2563eb);
|
background: var(--accent, #3b82f6);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Scrollbar markers container */
|
/* Scrollbar markers container */
|
||||||
|
|
@ -237,31 +239,16 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.user-message {
|
.user-message {
|
||||||
background: var(--accent, var(--primary, #3b82f6));
|
background: var(--chat-color1, var(--accent, var(--primary, #3b82f6)));
|
||||||
color: #ffffff !important;
|
color: #ffffff !important;
|
||||||
border-bottom-right-radius: 4px;
|
border-bottom-right-radius: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Light accent themes need dark text on user messages */
|
|
||||||
[data-theme="sentient"] .user-message,
|
|
||||||
[data-theme="y2kglow"] .user-message,
|
|
||||||
[data-theme="arcadeflash"] .user-message,
|
|
||||||
[data-theme="green"] .user-message,
|
|
||||||
[data-theme="jazzage"] .user-message,
|
|
||||||
[data-theme="mellowgold"] .user-message,
|
|
||||||
[data-theme="polaroidmemories"] .user-message,
|
|
||||||
[data-theme="seasidepostcard"] .user-message,
|
|
||||||
[data-theme="saturdaycartoons"] .user-message,
|
|
||||||
[data-theme="light"] .user-message,
|
|
||||||
[data-theme="typewriter"] .user-message,
|
|
||||||
[data-theme="3dbevel"] .user-message {
|
|
||||||
color: #000000 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.bot-message {
|
.bot-message {
|
||||||
background: var(--surface, var(--card, #2a2a2a));
|
background: var(--chat-color2, var(--surface, var(--card, #2a2a2a)));
|
||||||
color: #ffffff !important;
|
color: #000000 !important;
|
||||||
border-bottom-left-radius: 4px;
|
border-bottom-left-radius: 4px;
|
||||||
|
border: 1px solid var(--chat-color1, rgba(0, 0, 0, 0.2));
|
||||||
}
|
}
|
||||||
|
|
||||||
.bot-message,
|
.bot-message,
|
||||||
|
|
@ -275,56 +262,6 @@
|
||||||
.bot-message h2,
|
.bot-message h2,
|
||||||
.bot-message h3,
|
.bot-message h3,
|
||||||
.bot-message h4 {
|
.bot-message h4 {
|
||||||
color: #ffffff !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Light background themes need dark bot message text */
|
|
||||||
[data-theme="light"] .bot-message,
|
|
||||||
[data-theme="light"] .bot-message p,
|
|
||||||
[data-theme="light"] .bot-message span,
|
|
||||||
[data-theme="light"] .bot-message li,
|
|
||||||
[data-theme="light"] .bot-message a,
|
|
||||||
[data-theme="light"] .bot-message strong,
|
|
||||||
[data-theme="light"] .bot-message em,
|
|
||||||
[data-theme="light"] .bot-message h1,
|
|
||||||
[data-theme="light"] .bot-message h2,
|
|
||||||
[data-theme="light"] .bot-message h3,
|
|
||||||
[data-theme="light"] .bot-message h4,
|
|
||||||
[data-theme="polaroidmemories"] .bot-message,
|
|
||||||
[data-theme="polaroidmemories"] .bot-message p,
|
|
||||||
[data-theme="polaroidmemories"] .bot-message span,
|
|
||||||
[data-theme="polaroidmemories"] .bot-message li,
|
|
||||||
[data-theme="polaroidmemories"] .bot-message a,
|
|
||||||
[data-theme="polaroidmemories"] .bot-message h1,
|
|
||||||
[data-theme="polaroidmemories"] .bot-message h2,
|
|
||||||
[data-theme="seasidepostcard"] .bot-message,
|
|
||||||
[data-theme="seasidepostcard"] .bot-message p,
|
|
||||||
[data-theme="seasidepostcard"] .bot-message span,
|
|
||||||
[data-theme="seasidepostcard"] .bot-message li,
|
|
||||||
[data-theme="seasidepostcard"] .bot-message a,
|
|
||||||
[data-theme="seasidepostcard"] .bot-message h1,
|
|
||||||
[data-theme="seasidepostcard"] .bot-message h2,
|
|
||||||
[data-theme="saturdaycartoons"] .bot-message,
|
|
||||||
[data-theme="saturdaycartoons"] .bot-message p,
|
|
||||||
[data-theme="saturdaycartoons"] .bot-message span,
|
|
||||||
[data-theme="saturdaycartoons"] .bot-message li,
|
|
||||||
[data-theme="saturdaycartoons"] .bot-message a,
|
|
||||||
[data-theme="saturdaycartoons"] .bot-message h1,
|
|
||||||
[data-theme="saturdaycartoons"] .bot-message h2,
|
|
||||||
[data-theme="typewriter"] .bot-message,
|
|
||||||
[data-theme="typewriter"] .bot-message p,
|
|
||||||
[data-theme="typewriter"] .bot-message span,
|
|
||||||
[data-theme="typewriter"] .bot-message li,
|
|
||||||
[data-theme="typewriter"] .bot-message a,
|
|
||||||
[data-theme="typewriter"] .bot-message h1,
|
|
||||||
[data-theme="typewriter"] .bot-message h2,
|
|
||||||
[data-theme="3dbevel"] .bot-message,
|
|
||||||
[data-theme="3dbevel"] .bot-message p,
|
|
||||||
[data-theme="3dbevel"] .bot-message span,
|
|
||||||
[data-theme="3dbevel"] .bot-message li,
|
|
||||||
[data-theme="3dbevel"] .bot-message a,
|
|
||||||
[data-theme="3dbevel"] .bot-message h1,
|
|
||||||
[data-theme="3dbevel"] .bot-message h2 {
|
|
||||||
color: #000000 !important;
|
color: #000000 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -603,23 +540,61 @@ footer {
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
margin-bottom: 12px;
|
margin-bottom: 12px;
|
||||||
|
animation: slideIn 0.3s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.suggestion-chip,
|
||||||
.suggestion-button {
|
.suggestion-button {
|
||||||
padding: 6px 12px;
|
padding: 10px 18px;
|
||||||
border-radius: 16px;
|
border-radius: 24px;
|
||||||
border: 1px solid var(--border-color, #e5e7eb);
|
border: 2px solid var(--chat-color1, var(--suggestion-color, #4a9eff));
|
||||||
background: var(--secondary-bg, #f9fafb);
|
background: var(--chat-color2, rgba(255, 255, 255, 0.15));
|
||||||
color: var(--text-primary, #374151);
|
color: var(--chat-color1, #ffffff);
|
||||||
font-size: 13px;
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: all 0.2s;
|
transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
min-height: 40px;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
|
||||||
|
text-shadow: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.suggestion-chip::before,
|
||||||
|
.suggestion-button::before {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
background: var(--chat-color1, var(--accent, #3b82f6));
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.25s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
border-radius: inherit;
|
||||||
|
z-index: -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.suggestion-chip:hover,
|
||||||
.suggestion-button:hover {
|
.suggestion-button:hover {
|
||||||
background: var(--accent-color, #3b82f6);
|
background: var(--chat-color1, var(--suggestion-color, #4a9eff));
|
||||||
color: white;
|
color: #ffffff;
|
||||||
border-color: var(--accent-color, #3b82f6);
|
border-color: var(--chat-color1, var(--suggestion-color, #6bb3ff));
|
||||||
|
transform: translateY(-2px) scale(1.02);
|
||||||
|
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.4);
|
||||||
|
text-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.suggestion-chip:hover::before,
|
||||||
|
.suggestion-button:hover::before {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.suggestion-chip:active,
|
||||||
|
.suggestion-button:active {
|
||||||
|
transform: translateY(0);
|
||||||
|
box-shadow: 0 2px 6px rgba(59, 130, 246, 0.2);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Input Container */
|
/* Input Container */
|
||||||
|
|
@ -737,28 +712,38 @@ form.input-container {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
bottom: 100px;
|
bottom: 100px;
|
||||||
right: 20px;
|
right: 20px;
|
||||||
width: 40px;
|
width: 44px;
|
||||||
height: 40px;
|
height: 44px;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
border: 1px solid var(--border-color, #e5e7eb);
|
border: none;
|
||||||
background: var(--primary-bg, #ffffff);
|
background: var(--accent, #3b82f6);
|
||||||
color: var(--text-primary, #374151);
|
color: white;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
display: none;
|
display: none;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
font-size: 18px;
|
font-size: 20px;
|
||||||
transition: all 0.2s;
|
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
box-shadow: var(--shadow-sm, 0 2px 8px rgba(0, 0, 0, 0.1));
|
box-shadow: 0 4px 16px rgba(59, 130, 246, 0.3);
|
||||||
z-index: 100;
|
z-index: 100;
|
||||||
|
opacity: 0;
|
||||||
|
transform: scale(0.8) translateY(10px);
|
||||||
}
|
}
|
||||||
|
|
||||||
.scroll-to-bottom.visible {
|
.scroll-to-bottom.visible {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
opacity: 1;
|
||||||
|
transform: scale(1) translateY(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
.scroll-to-bottom:hover {
|
.scroll-to-bottom:hover {
|
||||||
background: var(--bg-hover, #f3f4f6);
|
background: var(--accent-hover, #2563eb);
|
||||||
|
transform: scale(1.1);
|
||||||
|
box-shadow: 0 6px 24px rgba(59, 130, 246, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.scroll-to-bottom:active {
|
||||||
|
transform: scale(0.95);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Responsive */
|
/* Responsive */
|
||||||
|
|
@ -1246,3 +1231,207 @@ form.input-container {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ===== Enhanced Chat Elements ===== */
|
||||||
|
|
||||||
|
/* Thinking Indicator */
|
||||||
|
.thinking-indicator {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
padding: 12px 16px;
|
||||||
|
background: var(--surface, rgba(42, 42, 42, 0.8));
|
||||||
|
border-radius: 16px;
|
||||||
|
animation: fadeIn 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.thinking-dots {
|
||||||
|
display: flex;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.thinking-dot {
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
background: var(--accent, #3b82f6);
|
||||||
|
border-radius: 50%;
|
||||||
|
animation: thinkingBounce 1.4s infinite ease-in-out both;
|
||||||
|
}
|
||||||
|
|
||||||
|
.thinking-dot:nth-child(1) {
|
||||||
|
animation-delay: -0.32s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.thinking-dot:nth-child(2) {
|
||||||
|
animation-delay: -0.16s;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes thinkingBounce {
|
||||||
|
0%, 80%, 100% {
|
||||||
|
transform: scale(0.8);
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
40% {
|
||||||
|
transform: scale(1);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Connection Status */
|
||||||
|
.connection-status {
|
||||||
|
position: fixed;
|
||||||
|
top: 20px;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 8px 16px;
|
||||||
|
background: var(--surface, rgba(26, 26, 36, 0.95));
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
border: 1px solid var(--border, rgba(42, 42, 42, 0.5));
|
||||||
|
border-radius: 24px;
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--text, #ffffff);
|
||||||
|
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
|
||||||
|
z-index: 1000;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.connection-status-dot {
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
border-radius: 50%;
|
||||||
|
animation: pulse 2s infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.connection-status.connected .connection-status-dot {
|
||||||
|
background: #22c55e;
|
||||||
|
box-shadow: 0 0 8px rgba(34, 197, 94, 0.6);
|
||||||
|
}
|
||||||
|
|
||||||
|
.connection-status.disconnected .connection-status-dot {
|
||||||
|
background: #ef4444;
|
||||||
|
box-shadow: 0 0 8px rgba(239, 68, 68, 0.6);
|
||||||
|
animation: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.connection-status.connecting .connection-status-dot {
|
||||||
|
background: #f59e0b;
|
||||||
|
box-shadow: 0 0 8px rgba(245, 158, 11, 0.6);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Message Animations */
|
||||||
|
@keyframes messageIn {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(20px) scale(0.95);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0) scale(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.message {
|
||||||
|
animation: messageIn 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Bot Message Glow Effect */
|
||||||
|
.message.bot .message-content {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message.bot .message-content::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
inset: -2px;
|
||||||
|
background: var(--accent-glow, rgba(59, 130, 246, 0.1));
|
||||||
|
border-radius: 18px;
|
||||||
|
z-index: -1;
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message.bot:hover .message-content::before {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Enhanced User Message */
|
||||||
|
.message.user .message-content {
|
||||||
|
box-shadow: 0 2px 12px rgba(59, 130, 246, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.message.user:hover .message-content {
|
||||||
|
box-shadow: 0 4px 20px rgba(59, 130, 246, 0.3);
|
||||||
|
transform: translateY(-1px);
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Smooth Scrolling */
|
||||||
|
#messages {
|
||||||
|
scroll-behavior: smooth;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* New Message Indicator */
|
||||||
|
.new-message-indicator {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 100%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
padding: 6px 12px;
|
||||||
|
background: var(--accent, #3b82f6);
|
||||||
|
color: white;
|
||||||
|
border-radius: 12px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 500;
|
||||||
|
white-space: nowrap;
|
||||||
|
animation: bounce 0.5s ease;
|
||||||
|
cursor: pointer;
|
||||||
|
box-shadow: 0 2px 12px rgba(59, 130, 246, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes bounce {
|
||||||
|
0%, 100% {
|
||||||
|
transform: translateX(-50%) translateY(0);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
transform: translateX(-50%) translateY(-5px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Typing Indicator */
|
||||||
|
.typing-indicator {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
padding: 8px 12px;
|
||||||
|
background: var(--surface, #2a2a2a);
|
||||||
|
border-radius: 12px;
|
||||||
|
margin-left: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.typing-indicator span {
|
||||||
|
width: 6px;
|
||||||
|
height: 6px;
|
||||||
|
background: var(--text-secondary, #888);
|
||||||
|
border-radius: 50%;
|
||||||
|
animation: typing 1.4s infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.typing-indicator span:nth-child(2) {
|
||||||
|
animation-delay: 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.typing-indicator span:nth-child(3) {
|
||||||
|
animation-delay: 0.4s;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes typing {
|
||||||
|
0%, 60%, 100% {
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
30% {
|
||||||
|
transform: translateY(-8px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,12 @@
|
||||||
<link rel="stylesheet" href="/suite/chat/chat.css" />
|
<link rel="stylesheet" href="/suite/chat/chat.css" />
|
||||||
|
|
||||||
<div class="chat-layout" id="chat-app">
|
<div class="chat-layout" id="chat-app">
|
||||||
|
<!-- Connection Status -->
|
||||||
|
<div class="connection-status connecting" id="connectionStatus" style="display: none;">
|
||||||
|
<span class="connection-status-dot"></span>
|
||||||
|
<span class="connection-text">Connecting...</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
<main id="messages"></main>
|
<main id="messages"></main>
|
||||||
|
|
||||||
<footer>
|
<footer>
|
||||||
|
|
@ -42,7 +48,11 @@
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
</footer>
|
</footer>
|
||||||
<button class="scroll-to-bottom" id="scrollToBottom">↓</button>
|
<button class="scroll-to-bottom" id="scrollToBottom" title="Scroll to bottom">
|
||||||
|
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<polyline points="6 9 12 15 18 9"></polyline>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="entity-card-tooltip" id="entityCardTooltip">
|
<div class="entity-card-tooltip" id="entityCardTooltip">
|
||||||
|
|
@ -155,6 +165,7 @@
|
||||||
var currentStreamingContent = "";
|
var currentStreamingContent = "";
|
||||||
var reconnectAttempts = 0;
|
var reconnectAttempts = 0;
|
||||||
var maxReconnectAttempts = 5;
|
var maxReconnectAttempts = 5;
|
||||||
|
var isUserScrolling = false;
|
||||||
|
|
||||||
var mentionState = {
|
var mentionState = {
|
||||||
active: false,
|
active: false,
|
||||||
|
|
@ -170,6 +181,61 @@
|
||||||
return div.innerHTML;
|
return div.innerHTML;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Scroll handling
|
||||||
|
function scrollToBottom(animate) {
|
||||||
|
var messages = document.getElementById("messages");
|
||||||
|
if (messages) {
|
||||||
|
if (animate) {
|
||||||
|
messages.scrollTo({
|
||||||
|
top: messages.scrollHeight,
|
||||||
|
behavior: "smooth",
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
messages.scrollTop = messages.scrollHeight;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateScrollButton() {
|
||||||
|
var messages = document.getElementById("messages");
|
||||||
|
var scrollBtn = document.getElementById("scrollToBottom");
|
||||||
|
if (!messages || !scrollBtn) return;
|
||||||
|
|
||||||
|
var isNearBottom =
|
||||||
|
messages.scrollHeight - messages.scrollTop - messages.clientHeight <
|
||||||
|
100;
|
||||||
|
|
||||||
|
if (isNearBottom) {
|
||||||
|
scrollBtn.classList.remove("visible");
|
||||||
|
} else {
|
||||||
|
scrollBtn.classList.add("visible");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Scroll-to-bottom button click
|
||||||
|
var scrollBtn = document.getElementById("scrollToBottom");
|
||||||
|
if (scrollBtn) {
|
||||||
|
scrollBtn.addEventListener("click", function () {
|
||||||
|
scrollToBottom(true);
|
||||||
|
isUserScrolling = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Detect user scrolling
|
||||||
|
var messagesEl = document.getElementById("messages");
|
||||||
|
if (messagesEl) {
|
||||||
|
messagesEl.addEventListener("scroll", function () {
|
||||||
|
isUserScrolling = true;
|
||||||
|
updateScrollButton();
|
||||||
|
|
||||||
|
// Reset isUserScrolling after 2 seconds of no scrolling
|
||||||
|
clearTimeout(messagesEl.scrollTimeout);
|
||||||
|
messagesEl.scrollTimeout = setTimeout(function () {
|
||||||
|
isUserScrolling = false;
|
||||||
|
}, 2000);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function renderMentionInMessage(content) {
|
function renderMentionInMessage(content) {
|
||||||
return content.replace(
|
return content.replace(
|
||||||
/@(\w+):([^\s]+)/g,
|
/@(\w+):([^\s]+)/g,
|
||||||
|
|
@ -227,7 +293,13 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
messages.appendChild(div);
|
messages.appendChild(div);
|
||||||
messages.scrollTop = messages.scrollHeight;
|
|
||||||
|
// Auto-scroll to bottom unless user is manually scrolling
|
||||||
|
if (!isUserScrolling) {
|
||||||
|
scrollToBottom(true);
|
||||||
|
} else {
|
||||||
|
updateScrollButton();
|
||||||
|
}
|
||||||
|
|
||||||
setupMentionClickHandlers(div);
|
setupMentionClickHandlers(div);
|
||||||
}
|
}
|
||||||
|
|
@ -675,6 +747,11 @@
|
||||||
addMessage("bot", data.content);
|
addMessage("bot", data.content);
|
||||||
}
|
}
|
||||||
isStreaming = false;
|
isStreaming = false;
|
||||||
|
|
||||||
|
// Render suggestions when message is complete
|
||||||
|
if (data.suggestions && Array.isArray(data.suggestions) && data.suggestions.length > 0) {
|
||||||
|
renderSuggestions(data.suggestions);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
if (!isStreaming) {
|
if (!isStreaming) {
|
||||||
isStreaming = true;
|
isStreaming = true;
|
||||||
|
|
@ -692,22 +769,83 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function sendMessage() {
|
// Render suggestion buttons
|
||||||
|
function renderSuggestions(suggestions) {
|
||||||
|
var suggestionsEl = document.getElementById("suggestions");
|
||||||
|
if (!suggestionsEl) {
|
||||||
|
console.warn("Suggestions container not found");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear existing suggestions
|
||||||
|
suggestionsEl.innerHTML = "";
|
||||||
|
|
||||||
|
console.log("Rendering " + suggestions.length + " suggestions");
|
||||||
|
|
||||||
|
suggestions.forEach(function (suggestion) {
|
||||||
|
var chip = document.createElement("button");
|
||||||
|
chip.className = "suggestion-chip";
|
||||||
|
chip.textContent = suggestion.text || "Suggestion";
|
||||||
|
|
||||||
|
// Use window.sendMessage which is already exposed
|
||||||
|
chip.onclick = (function(sugg) {
|
||||||
|
return function() {
|
||||||
|
console.log("Suggestion clicked:", sugg);
|
||||||
|
// Check if there's an action to parse
|
||||||
|
if (sugg.action) {
|
||||||
|
try {
|
||||||
|
var action = typeof sugg.action === "string"
|
||||||
|
? JSON.parse(sugg.action)
|
||||||
|
: sugg.action;
|
||||||
|
|
||||||
|
console.log("Parsed action:", action);
|
||||||
|
|
||||||
|
if (action.type === "invoke_tool") {
|
||||||
|
// Send the tool name as text - the backend will handle tool invocation
|
||||||
|
window.sendMessage(action.tool);
|
||||||
|
} else if (action.type === "send_message") {
|
||||||
|
window.sendMessage(action.message || sugg.text);
|
||||||
|
} else if (action.type === "select_context") {
|
||||||
|
window.sendMessage(action.context);
|
||||||
|
} else {
|
||||||
|
window.sendMessage(sugg.text);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Failed to parse action:", e, "falling back to text");
|
||||||
|
window.sendMessage(sugg.text);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// No action, just send the text
|
||||||
|
window.sendMessage(sugg.text);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
})(suggestion);
|
||||||
|
|
||||||
|
suggestionsEl.appendChild(chip);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function sendMessage(messageContent) {
|
||||||
var input = document.getElementById("messageInput");
|
var input = document.getElementById("messageInput");
|
||||||
if (!input) {
|
if (!input) {
|
||||||
console.error("Chat input not found");
|
console.error("Chat input not found");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var content = input.value.trim();
|
// If no messageContent provided, read from input
|
||||||
|
var content = messageContent || input.value.trim();
|
||||||
if (!content) {
|
if (!content) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
hideMentionDropdown();
|
// If called from input field (no messageContent provided), clear input
|
||||||
|
if (!messageContent) {
|
||||||
|
hideMentionDropdown();
|
||||||
|
input.value = "";
|
||||||
|
input.focus();
|
||||||
|
}
|
||||||
|
|
||||||
addMessage("user", content);
|
addMessage("user", content);
|
||||||
input.value = "";
|
|
||||||
input.focus();
|
|
||||||
|
|
||||||
if (ws && ws.readyState === WebSocket.OPEN) {
|
if (ws && ws.readyState === WebSocket.OPEN) {
|
||||||
ws.send(
|
ws.send(
|
||||||
|
|
@ -728,11 +866,24 @@
|
||||||
|
|
||||||
window.sendMessage = sendMessage;
|
window.sendMessage = sendMessage;
|
||||||
|
|
||||||
|
// Expose session info for suggestion clicks
|
||||||
|
window.getChatSessionInfo = function() {
|
||||||
|
return {
|
||||||
|
ws: ws,
|
||||||
|
currentBotId: currentBotId,
|
||||||
|
currentUserId: currentUserId,
|
||||||
|
currentSessionId: currentSessionId,
|
||||||
|
currentBotName: currentBotName
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
function connectWebSocket() {
|
function connectWebSocket() {
|
||||||
if (ws) {
|
if (ws) {
|
||||||
ws.close();
|
ws.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
updateConnectionStatus("connecting");
|
||||||
|
|
||||||
var url =
|
var url =
|
||||||
WS_URL +
|
WS_URL +
|
||||||
"?session_id=" +
|
"?session_id=" +
|
||||||
|
|
@ -746,6 +897,7 @@
|
||||||
ws.onopen = function () {
|
ws.onopen = function () {
|
||||||
console.log("WebSocket connected");
|
console.log("WebSocket connected");
|
||||||
reconnectAttempts = 0;
|
reconnectAttempts = 0;
|
||||||
|
updateConnectionStatus("connected");
|
||||||
};
|
};
|
||||||
|
|
||||||
ws.onmessage = function (event) {
|
ws.onmessage = function (event) {
|
||||||
|
|
@ -756,13 +908,11 @@
|
||||||
// Ignore connection confirmation
|
// Ignore connection confirmation
|
||||||
if (data.type === "connected") return;
|
if (data.type === "connected") return;
|
||||||
|
|
||||||
// Ignore system events (theme changes, etc)
|
// Process system events (theme changes, etc)
|
||||||
if (data.event) {
|
if (data.event) {
|
||||||
console.log(
|
if (data.event === "change_theme") {
|
||||||
"System event received, ignoring:",
|
applyThemeData(data.data || {});
|
||||||
data.event,
|
}
|
||||||
data,
|
|
||||||
);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -771,10 +921,7 @@
|
||||||
try {
|
try {
|
||||||
var contentObj = JSON.parse(data.content);
|
var contentObj = JSON.parse(data.content);
|
||||||
if (contentObj.event === "change_theme") {
|
if (contentObj.event === "change_theme") {
|
||||||
console.log(
|
applyThemeData(contentObj.data || {});
|
||||||
"Theme change event in content, ignoring:",
|
|
||||||
contentObj,
|
|
||||||
);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|
@ -795,19 +942,83 @@
|
||||||
};
|
};
|
||||||
|
|
||||||
ws.onclose = function () {
|
ws.onclose = function () {
|
||||||
|
updateConnectionStatus("disconnected");
|
||||||
notify("Disconnected from chat server", "error");
|
notify("Disconnected from chat server", "error");
|
||||||
if (reconnectAttempts < maxReconnectAttempts) {
|
if (reconnectAttempts < maxReconnectAttempts) {
|
||||||
reconnectAttempts++;
|
reconnectAttempts++;
|
||||||
|
updateConnectionStatus("connecting");
|
||||||
setTimeout(connectWebSocket, 1000 * reconnectAttempts);
|
setTimeout(connectWebSocket, 1000 * reconnectAttempts);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
ws.onerror = function (e) {
|
ws.onerror = function (e) {
|
||||||
console.error("WebSocket error:", e);
|
console.error("WebSocket error:", e);
|
||||||
|
updateConnectionStatus("disconnected");
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Apply theme data from WebSocket events
|
||||||
|
function applyThemeData(themeData) {
|
||||||
|
console.log("Applying theme data:", themeData);
|
||||||
|
|
||||||
|
var color1 = themeData.color1 || themeData.data?.color1 || "#3b82f6";
|
||||||
|
var color2 = themeData.color2 || themeData.data?.color2 || "#f5deb3";
|
||||||
|
var logo = themeData.logo_url || themeData.data?.logo_url || "";
|
||||||
|
var title = themeData.title || themeData.data?.title || window.__INITIAL_BOT_NAME__ || "Chat";
|
||||||
|
|
||||||
|
// Set CSS variables for colors on document element
|
||||||
|
document.documentElement.style.setProperty("--chat-color1", color1);
|
||||||
|
document.documentElement.style.setProperty("--chat-color2", color2);
|
||||||
|
document.documentElement.style.setProperty("--suggestion-color", color1);
|
||||||
|
document.documentElement.style.setProperty("--suggestion-bg", color2);
|
||||||
|
|
||||||
|
// Also set on root for better cascading
|
||||||
|
document.documentElement.style.setProperty("--color1", color1);
|
||||||
|
document.documentElement.style.setProperty("--color2", color2);
|
||||||
|
|
||||||
|
// Update suggestion button colors to match theme
|
||||||
|
document.documentElement.style.setProperty("--primary", color1);
|
||||||
|
document.documentElement.style.setProperty("--accent", color1);
|
||||||
|
|
||||||
|
console.log("Theme applied:", { color1: color1, color2: color2, logo: logo, title: title });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load bot config and apply colors/logo
|
||||||
|
function loadBotConfig() {
|
||||||
|
var botName = window.__INITIAL_BOT_NAME__ || "default";
|
||||||
|
|
||||||
|
fetch("/api/bot/config?bot_name=" + encodeURIComponent(botName))
|
||||||
|
.then(function(response) {
|
||||||
|
return response.json();
|
||||||
|
})
|
||||||
|
.then(function(config) {
|
||||||
|
if (!config) return;
|
||||||
|
|
||||||
|
// Apply colors from config
|
||||||
|
var color1 = config["theme-color1"] || config["Theme Color"] || "#3b82f6";
|
||||||
|
var color2 = config["theme-color2"] || "#f5deb3";
|
||||||
|
var title = config["theme-title"] || botName;
|
||||||
|
|
||||||
|
// Set CSS variables for colors on document element
|
||||||
|
document.documentElement.style.setProperty("--chat-color1", color1);
|
||||||
|
document.documentElement.style.setProperty("--chat-color2", color2);
|
||||||
|
document.documentElement.style.setProperty("--suggestion-color", color1);
|
||||||
|
document.documentElement.style.setProperty("--suggestion-bg", color2);
|
||||||
|
document.documentElement.style.setProperty("--color1", color1);
|
||||||
|
document.documentElement.style.setProperty("--color2", color2);
|
||||||
|
document.documentElement.style.setProperty("--primary", color1);
|
||||||
|
document.documentElement.style.setProperty("--accent", color1);
|
||||||
|
|
||||||
|
console.log("Bot config loaded:", { color1: color1, color2: color2, title: title });
|
||||||
|
})
|
||||||
|
.catch(function(e) {
|
||||||
|
console.log("Could not load bot config:", e);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function initChat() {
|
function initChat() {
|
||||||
|
// Load bot config first
|
||||||
|
loadBotConfig();
|
||||||
// Just proceed with chat initialization - no auth check
|
// Just proceed with chat initialization - no auth check
|
||||||
proceedWithChatInit();
|
proceedWithChatInit();
|
||||||
}
|
}
|
||||||
|
|
@ -838,6 +1049,31 @@
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function updateConnectionStatus(status) {
|
||||||
|
var statusEl = document.getElementById("connectionStatus");
|
||||||
|
if (!statusEl) return;
|
||||||
|
|
||||||
|
statusEl.className = "connection-status " + status;
|
||||||
|
|
||||||
|
var statusText = statusEl.querySelector(".connection-text");
|
||||||
|
if (statusText) {
|
||||||
|
switch (status) {
|
||||||
|
case "connected":
|
||||||
|
statusText.textContent = "Connected";
|
||||||
|
statusEl.style.display = "none";
|
||||||
|
break;
|
||||||
|
case "disconnected":
|
||||||
|
statusText.textContent = "Disconnected";
|
||||||
|
statusEl.style.display = "flex";
|
||||||
|
break;
|
||||||
|
case "connecting":
|
||||||
|
statusText.textContent = "Connecting...";
|
||||||
|
statusEl.style.display = "flex";
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function setupEventHandlers() {
|
function setupEventHandlers() {
|
||||||
var form = document.getElementById("chatForm");
|
var form = document.getElementById("chatForm");
|
||||||
var input = document.getElementById("messageInput");
|
var input = document.getElementById("messageInput");
|
||||||
|
|
|
||||||
16
ui/suite/css/config-colors.css
Normal file
16
ui/suite/css/config-colors.css
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
/* Config Color Overrides */
|
||||||
|
/* Maps theme-color1 and theme-color2 from config.csv to actual theme variables */
|
||||||
|
|
||||||
|
:root {
|
||||||
|
/* Use --color1 and --color2 from config.csv, with fallback defaults */
|
||||||
|
--sentient-accent: var(--color1, #3b82f6);
|
||||||
|
--primary: var(--color1, #3b82f6);
|
||||||
|
--primary-hover: color-mix(in srgb, var(--color1, #3b82f6) 85%, black);
|
||||||
|
--primary-light: color-mix(in srgb, var(--color1, #3b82f6) 10%, transparent);
|
||||||
|
--chart-1: var(--color1, #3b82f6);
|
||||||
|
--chart-2: var(--color2, #f59e0b);
|
||||||
|
--ring: var(--color1, #3b82f6);
|
||||||
|
|
||||||
|
/* Background can use color2 for subtle tint */
|
||||||
|
/* --sentient-bg-primary stays white/light for text readability */
|
||||||
|
}
|
||||||
|
|
@ -50,10 +50,10 @@
|
||||||
|
|
||||||
<!-- SECURITY BOOTSTRAP - MUST load immediately after HTMX -->
|
<!-- SECURITY BOOTSTRAP - MUST load immediately after HTMX -->
|
||||||
<!-- This provides centralized auth for ALL apps: HTMX, fetch, XHR -->
|
<!-- This provides centralized auth for ALL apps: HTMX, fetch, XHR -->
|
||||||
<script src="suite/js/security-bootstrap.js?v=20260110"></script>
|
<script src="suite/js/security-bootstrap.js?v=20260207b"></script>
|
||||||
|
|
||||||
<!-- ERROR REPORTER - Captures JS errors and sends to server log -->
|
<!-- ERROR REPORTER - Captures JS errors and sends to server log -->
|
||||||
<script src="suite/js/error-reporter.js"></script>
|
<script src="suite/js/error-reporter.js?v=20260207c"></script>
|
||||||
|
|
||||||
<!-- i18n -->
|
<!-- i18n -->
|
||||||
<script src="suite/js/i18n.js"></script>
|
<script src="suite/js/i18n.js"></script>
|
||||||
|
|
|
||||||
|
|
@ -36,13 +36,15 @@
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
console.warn('[ErrorReporter] Failed to send errors:', response.status);
|
console.warn('[ErrorReporter] Failed to send errors:', response.status);
|
||||||
|
} else {
|
||||||
|
console.log('[ErrorReporter] Sent', errorsToReport.length, 'errors to server');
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn('[ErrorReporter] Failed to send errors:', e.message);
|
console.warn('[ErrorReporter] Failed to send errors:', e.message);
|
||||||
errorQueue.unshift(...errorsToReport);
|
errorQueue.unshift(...errorsToReport);
|
||||||
} finally {
|
} finally {
|
||||||
isReporting = false;
|
isReporting = false;
|
||||||
|
|
||||||
if (errorQueue.length > 0) {
|
if (errorQueue.length > 0) {
|
||||||
setTimeout(reportErrors, 1000);
|
setTimeout(reportErrors, 1000);
|
||||||
}
|
}
|
||||||
|
|
@ -76,6 +78,15 @@
|
||||||
report: function(error, context) {
|
report: function(error, context) {
|
||||||
queueError(formatError(error, context));
|
queueError(formatError(error, context));
|
||||||
},
|
},
|
||||||
|
reportNetworkError: function(url, status, statusText) {
|
||||||
|
queueError({
|
||||||
|
type: 'NetworkError',
|
||||||
|
message: `Failed to load ${url}: ${status} ${statusText}`,
|
||||||
|
url: window.location.href,
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
context: { url, status, statusText }
|
||||||
|
});
|
||||||
|
},
|
||||||
flush: function() {
|
flush: function() {
|
||||||
reportErrors();
|
reportErrors();
|
||||||
}
|
}
|
||||||
|
|
@ -101,7 +112,7 @@
|
||||||
url: window.location.href,
|
url: window.location.href,
|
||||||
timestamp: new Date().toISOString()
|
timestamp: new Date().toISOString()
|
||||||
};
|
};
|
||||||
|
|
||||||
queueError({
|
queueError({
|
||||||
name: 'Navigation',
|
name: 'Navigation',
|
||||||
message: `${method}: ${from} -> ${to}`,
|
message: `${method}: ${from} -> ${to}`,
|
||||||
|
|
@ -110,26 +121,70 @@
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
document.body.addEventListener('click', function(e) {
|
function initNavigationTracking() {
|
||||||
const target = e.target.closest('[data-section]');
|
if (!document.body) {
|
||||||
if (target) {
|
setTimeout(initNavigationTracking, 50);
|
||||||
const section = target.getAttribute('data-section');
|
return;
|
||||||
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) {
|
if (document.body) {
|
||||||
const oldURL = new URL(e.oldURL);
|
document.body.addEventListener('click', function(e) {
|
||||||
const newURL = new URL(e.newURL);
|
const target = e.target.closest('[data-section]');
|
||||||
const fromHash = oldURL.hash.slice(1) || '';
|
if (target) {
|
||||||
const toHash = newURL.hash.slice(1) || '';
|
const section = target.getAttribute('data-section');
|
||||||
window.NavigationLogger.log(fromHash || 'home', toHash, 'hashchange');
|
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');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (document.readyState === 'loading') {
|
||||||
|
document.addEventListener('DOMContentLoaded', initNavigationTracking);
|
||||||
|
} else {
|
||||||
|
initNavigationTracking();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Intercept link onload/onerror events to catch CSS/image load failures
|
||||||
|
const originalCreateElement = document.createElement;
|
||||||
|
document.createElement = function(tagName) {
|
||||||
|
const element = originalCreateElement.call(document, tagName);
|
||||||
|
if (tagName.toLowerCase() === 'link') {
|
||||||
|
element.addEventListener('error', function() {
|
||||||
|
if (this.href && window.ErrorReporter && window.ErrorReporter.reportNetworkError) {
|
||||||
|
window.ErrorReporter.reportNetworkError(this.href, 'LOAD_FAILED', 'Resource failed to load');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return element;
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener('load', () => {
|
||||||
|
setTimeout(() => {
|
||||||
|
const failedResources = performance.getEntriesByType('resource').filter(entry =>
|
||||||
|
entry.transferSize === 0 && entry.decodedBodySize > 0 && !entry.name.includes('anon') && entry.duration > 100
|
||||||
|
);
|
||||||
|
|
||||||
|
if (failedResources.length > 0) {
|
||||||
|
console.warn('[ErrorReporter] Detected potentially failed resources:', failedResources);
|
||||||
|
failedResources.forEach(resource => {
|
||||||
|
window.ErrorReporter.reportNetworkError(resource.name, 'FAILED', 'Resource load timeout/failure');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, 5000);
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log('[NavigationLogger] Navigation tracking initialized');
|
|
||||||
})();
|
})();
|
||||||
|
|
|
||||||
|
|
@ -204,10 +204,18 @@
|
||||||
// Handle WebSocket messages
|
// Handle WebSocket messages
|
||||||
function handleWebSocketMessage(message) {
|
function handleWebSocketMessage(message) {
|
||||||
const messageType = message.type || message.event;
|
const messageType = message.type || message.event;
|
||||||
|
|
||||||
// Debug logging
|
// Debug logging
|
||||||
console.log("handleWebSocketMessage called with:", { messageType, message });
|
console.log("handleWebSocketMessage called with:", { messageType, message });
|
||||||
|
|
||||||
|
// Handle suggestions array from BotResponse
|
||||||
|
if (message.suggestions && Array.isArray(message.suggestions) && message.suggestions.length > 0) {
|
||||||
|
clearSuggestions();
|
||||||
|
message.suggestions.forEach(suggestion => {
|
||||||
|
addSuggestionButton(suggestion.text, suggestion.value || suggestion.text);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
switch (messageType) {
|
switch (messageType) {
|
||||||
case "message":
|
case "message":
|
||||||
appendMessage(message);
|
appendMessage(message);
|
||||||
|
|
@ -246,6 +254,31 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Clear all suggestions
|
||||||
|
function clearSuggestions() {
|
||||||
|
const suggestionsEl = document.getElementById("suggestions");
|
||||||
|
if (suggestionsEl) {
|
||||||
|
suggestionsEl.innerHTML = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add suggestion button with value
|
||||||
|
function addSuggestionButton(text, value) {
|
||||||
|
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: value }));
|
||||||
|
chip.setAttribute("hx-target", "#messages");
|
||||||
|
chip.setAttribute("hx-swap", "beforeend");
|
||||||
|
|
||||||
|
suggestionsEl.appendChild(chip);
|
||||||
|
htmx.process(chip);
|
||||||
|
}
|
||||||
|
|
||||||
// Append message to chat
|
// Append message to chat
|
||||||
function appendMessage(message) {
|
function appendMessage(message) {
|
||||||
const messagesEl = document.getElementById("messages");
|
const messagesEl = document.getElementById("messages");
|
||||||
|
|
|
||||||
|
|
@ -210,10 +210,14 @@
|
||||||
return originalFetch
|
return originalFetch
|
||||||
.call(window, input, init)
|
.call(window, input, init)
|
||||||
.then(function (response) {
|
.then(function (response) {
|
||||||
|
var url = typeof input === "string" ? input : input.url;
|
||||||
|
|
||||||
if (response.status === 401) {
|
if (response.status === 401) {
|
||||||
var url = typeof input === "string" ? input : input.url;
|
|
||||||
self.handleUnauthorized(url);
|
self.handleUnauthorized(url);
|
||||||
|
} else if (!response.ok && window.ErrorReporter && window.ErrorReporter.reportNetworkError) {
|
||||||
|
window.ErrorReporter.reportNetworkError(url, response.status, response.statusText);
|
||||||
}
|
}
|
||||||
|
|
||||||
return response;
|
return response;
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -698,15 +698,20 @@ 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")
|
// Detect bot name from pathname (e.g., /bot/cristo -> bot_name = "cristo", /edu -> bot_name = "edu")
|
||||||
const detectBotFromPath = () => {
|
const detectBotFromPath = () => {
|
||||||
const pathname = window.location.pathname;
|
const pathname = window.location.pathname;
|
||||||
// Remove leading/trailing slashes and get first segment
|
// Remove leading/trailing slashes and split
|
||||||
const segments = pathname.replace(/^\/|\/$/g, "").split("/");
|
const segments = pathname.replace(/^\/|\/$/g, "").split("/");
|
||||||
const firstSegment = segments[0];
|
|
||||||
|
|
||||||
// If first segment is not a known route, treat it as bot name
|
// Handle /bot/{bot_name} pattern
|
||||||
const knownRoutes = ["suite", "auth", "api", "static", "public"];
|
if (segments[0] === "bot" && segments[1]) {
|
||||||
|
return segments[1];
|
||||||
|
}
|
||||||
|
|
||||||
|
// For other patterns, use first segment if it's not a known route
|
||||||
|
const firstSegment = segments[0];
|
||||||
|
const knownRoutes = ["suite", "auth", "api", "static", "public", "bot"];
|
||||||
if (firstSegment && !knownRoutes.includes(firstSegment)) {
|
if (firstSegment && !knownRoutes.includes(firstSegment)) {
|
||||||
return firstSegment;
|
return firstSegment;
|
||||||
}
|
}
|
||||||
|
|
@ -1024,7 +1029,10 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (document.readyState === "complete") {
|
// Skip SPA initialization on auth pages (login, register, etc.)
|
||||||
|
if (window.location.pathname.startsWith("/auth/")) {
|
||||||
|
console.log("[SPA] Skipping initialization on auth page");
|
||||||
|
} else if (document.readyState === "complete") {
|
||||||
setTimeout(initialLoad, 50);
|
setTimeout(initialLoad, 50);
|
||||||
} else {
|
} else {
|
||||||
window.addEventListener("load", () => {
|
window.addEventListener("load", () => {
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,27 @@ const ThemeManager = (() => {
|
||||||
let currentThemeId = "default";
|
let currentThemeId = "default";
|
||||||
let subscribers = [];
|
let subscribers = [];
|
||||||
|
|
||||||
|
// Bot ID to theme mapping (configured via config.csv theme-base field)
|
||||||
|
const botThemeMap = {
|
||||||
|
// Default bot uses light theme with brown accents
|
||||||
|
"default": "light",
|
||||||
|
// Cristo bot uses mellowgold theme with earth tones
|
||||||
|
"cristo": "mellowgold",
|
||||||
|
// Salesianos bot uses light theme with blue accents
|
||||||
|
"salesianos": "light",
|
||||||
|
};
|
||||||
|
|
||||||
|
// Detect current bot from URL path
|
||||||
|
function getCurrentBotId() {
|
||||||
|
const path = window.location.pathname;
|
||||||
|
// Match patterns like /bot/cristo, /cristo, etc.
|
||||||
|
const match = path.match(/(?:\/bot\/)?([a-z0-9-]+)/i);
|
||||||
|
if (match && match[1]) {
|
||||||
|
return match[1].toLowerCase();
|
||||||
|
}
|
||||||
|
return "default";
|
||||||
|
}
|
||||||
|
|
||||||
const themes = [
|
const themes = [
|
||||||
{ id: "default", name: "🎨 Default", file: "light.css" },
|
{ id: "default", name: "🎨 Default", file: "light.css" },
|
||||||
{ id: "light", name: "☀️ Light", file: "light.css" },
|
{ id: "light", name: "☀️ Light", file: "light.css" },
|
||||||
|
|
@ -54,7 +75,7 @@ const ThemeManager = (() => {
|
||||||
const link = document.createElement("link");
|
const link = document.createElement("link");
|
||||||
link.id = "theme-css";
|
link.id = "theme-css";
|
||||||
link.rel = "stylesheet";
|
link.rel = "stylesheet";
|
||||||
link.href = `public/themes/${theme.file}`;
|
link.href = `/public/themes/${theme.file}`;
|
||||||
link.onload = () => {
|
link.onload = () => {
|
||||||
console.log("✓ Theme loaded:", theme.name);
|
console.log("✓ Theme loaded:", theme.name);
|
||||||
currentThemeId = id;
|
currentThemeId = id;
|
||||||
|
|
@ -87,7 +108,19 @@ const ThemeManager = (() => {
|
||||||
}
|
}
|
||||||
|
|
||||||
function init() {
|
function init() {
|
||||||
let saved = localStorage.getItem("gb-theme") || "default";
|
// First, load saved bot theme from config.csv (if available)
|
||||||
|
loadSavedTheme();
|
||||||
|
|
||||||
|
// Then load the UI theme (CSS theme)
|
||||||
|
// Priority: 1) localStorage user preference, 2) bot-specific theme, 3) default
|
||||||
|
let saved = localStorage.getItem("gb-theme");
|
||||||
|
if (!saved || !themes.find((t) => t.id === saved)) {
|
||||||
|
// No user preference, try bot-specific theme
|
||||||
|
const botId = getCurrentBotId();
|
||||||
|
saved = botThemeMap[botId] || "light";
|
||||||
|
// Save to localStorage so it persists
|
||||||
|
localStorage.setItem("gb-theme", saved);
|
||||||
|
}
|
||||||
if (!themes.find((t) => t.id === saved)) saved = "default";
|
if (!themes.find((t) => t.id === saved)) saved = "default";
|
||||||
currentThemeId = saved;
|
currentThemeId = saved;
|
||||||
loadTheme(saved);
|
loadTheme(saved);
|
||||||
|
|
@ -99,21 +132,56 @@ const ThemeManager = (() => {
|
||||||
}
|
}
|
||||||
|
|
||||||
function setThemeFromServer(data) {
|
function setThemeFromServer(data) {
|
||||||
|
// Save theme to localStorage for persistence across page loads
|
||||||
|
localStorage.setItem("gb-theme-data", JSON.stringify(data));
|
||||||
|
|
||||||
|
// Load base theme if specified
|
||||||
|
if (data.theme_base) {
|
||||||
|
loadTheme(data.theme_base);
|
||||||
|
}
|
||||||
|
|
||||||
if (data.logo_url) {
|
if (data.logo_url) {
|
||||||
document
|
document
|
||||||
.querySelectorAll(".logo-icon, .assistant-avatar")
|
.querySelectorAll(".logo-icon, .assistant-avatar")
|
||||||
.forEach((el) => {
|
.forEach((el) => {
|
||||||
el.style.backgroundImage = `url("${data.logo_url}")`;
|
el.style.backgroundImage = `url("${data.logo_url}")`;
|
||||||
|
el.style.backgroundSize = "contain";
|
||||||
|
el.style.backgroundRepeat = "no-repeat";
|
||||||
|
el.style.backgroundPosition = "center";
|
||||||
|
// Clear emoji text content when logo image is applied
|
||||||
|
if (el.classList.contains("logo-icon")) {
|
||||||
|
el.textContent = "";
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
if (data.color1) {
|
||||||
|
document.documentElement.style.setProperty("--color1", data.color1);
|
||||||
|
}
|
||||||
|
if (data.color2) {
|
||||||
|
document.documentElement.style.setProperty("--color2", data.color2);
|
||||||
|
}
|
||||||
if (data.title) document.title = data.title;
|
if (data.title) document.title = data.title;
|
||||||
if (data.logo_text) {
|
if (data.logo_text) {
|
||||||
document.querySelectorAll(".logo-text").forEach((el) => {
|
document.querySelectorAll(".logo span, .logo-text").forEach((el) => {
|
||||||
el.textContent = data.logo_text;
|
el.textContent = data.logo_text;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Load saved theme from localStorage on page load
|
||||||
|
function loadSavedTheme() {
|
||||||
|
const savedTheme = localStorage.getItem("gb-theme-data");
|
||||||
|
if (savedTheme) {
|
||||||
|
try {
|
||||||
|
const data = JSON.parse(savedTheme);
|
||||||
|
setThemeFromServer(data);
|
||||||
|
console.log("✓ Theme loaded from localStorage");
|
||||||
|
} catch (e) {
|
||||||
|
console.warn("Failed to load saved theme:", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function applyCustomizations() {
|
function applyCustomizations() {
|
||||||
// Called by modules if needed
|
// Called by modules if needed
|
||||||
}
|
}
|
||||||
|
|
@ -126,6 +194,7 @@ const ThemeManager = (() => {
|
||||||
init,
|
init,
|
||||||
loadTheme,
|
loadTheme,
|
||||||
setThemeFromServer,
|
setThemeFromServer,
|
||||||
|
loadSavedTheme,
|
||||||
applyCustomizations,
|
applyCustomizations,
|
||||||
subscribe,
|
subscribe,
|
||||||
getAvailableThemes: () => themes,
|
getAvailableThemes: () => themes,
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue