refactor(web): add SPA fallback route and clean up server

- Removed the unused `serve_html` handler and its import.
- Added a fallback static file service that serves `index.html` for any unmatched path, enabling proper SPA routing.
- Reordered service registration to place the fallback before the explicit index route.
- Cleaned up redundant blank lines and imports in `mod.rs`.

**Client-side (chat.js) updates**
- Renamed the message input variable from `input` to `messageInputEl` for clarity.
- Introduced `pendingContextChange` placeholder for future context handling.
- Switched initialization event from `document 'ready'` to `window 'load'`.
- Updated DOM element assignments and focus calls to use the new variable name.
- Removed unused sidebar auto‑close logic and obsolete session loading functions (`loadSessions`, `loadSessionHistory`).
- Minor refactoring and comment adjustments to improve readability and eliminate dead code.
This commit is contained in:
Rodrigo Rodriguez (Pragmatismo) 2025-11-17 12:11:13 -03:00
parent 41fd167d50
commit e38554ea51
4 changed files with 62 additions and 54 deletions

View file

@ -14,15 +14,6 @@ async fn index() -> Result<HttpResponse> {
}
}
async fn serve_html(path: &str) -> Result<HttpResponse> {
match fs::read_to_string(format!("web/desktop/{}", path)) {
Ok(html) => Ok(HttpResponse::Ok().content_type("text/html").body(html)),
Err(e) => {
error!("Failed to load page {}: {}", path, e);
Ok(HttpResponse::InternalServerError().body("Failed to load page"))
}
}
}
pub fn configure_app(cfg: &mut actix_web::web::ServiceConfig) {
let static_path = Path::new("./web/desktop");
@ -50,7 +41,6 @@ pub fn configure_app(cfg: &mut actix_web::web::ServiceConfig) {
.use_etag(true)
);
cfg.service(
Files::new("/chat", static_path.join("chat"))
.prefer_utf8(true)
@ -71,6 +61,15 @@ pub fn configure_app(cfg: &mut actix_web::web::ServiceConfig) {
.use_last_modified(true)
.use_etag(true)
);
cfg.service(index);
// Fallback: serve index.html for any other path to enable SPA routing
cfg.service(
Files::new("/", static_path)
.index_file("index.html")
.prefer_utf8(true)
.use_last_modified(true)
.use_etag(true)
);
cfg.service(index);
}

View file

@ -2,6 +2,7 @@ function chatApp() {
// Core state variables (shared via closure)
let ws = null,
pendingContextChange = null,
currentSessionId = null,
currentUserId = null,
currentBotId = "default_bot",
@ -29,7 +30,7 @@ function chatApp() {
const maxReconnectAttempts = 5;
// DOM references (cached for performance)
let messagesDiv, input, sendBtn, voiceBtn, connectionStatus, flashOverlay, suggestionsContainer, floatLogo, sidebar, themeBtn, scrollToBottomBtn, contextIndicator, contextPercentage, contextProgressBar, sidebarTitle;
let messagesDiv, messageInputEl, sendBtn, voiceBtn, connectionStatus, flashOverlay, suggestionsContainer, floatLogo, sidebar, themeBtn, scrollToBottomBtn, contextIndicator, contextPercentage, contextProgressBar, sidebarTitle;
marked.setOptions({ breaks: true, gfm: true });
@ -98,10 +99,11 @@ function chatApp() {
// Lifecycle / event handlers
// ----------------------------------------------------------------------
init() {
document.addEventListener('ready', () => {
window.addEventListener('load', () => {
// Assign DOM elements after the document is ready
messagesDiv = document.getElementById("messages");
input = document.getElementById("messageInput");
messageInputEl = document.getElementById("messageInput");
sendBtn = document.getElementById("sendBtn");
voiceBtn = document.getElementById("voiceBtn");
connectionStatus = document.getElementById("connectionStatus");
@ -125,13 +127,11 @@ function chatApp() {
this.applyTheme();
}
});
input.focus();
messageInputEl.focus();
// UI event listeners
document.addEventListener('click', (e) => {
if (sidebar.classList.contains('open') && !sidebar.contains(e.target) && !floatLogo.contains(e.target)) {
sidebar.classList.remove('open');
}
});
messagesDiv.addEventListener('scroll', () => {
@ -150,7 +150,7 @@ function chatApp() {
});
sendBtn.onclick = () => this.sendMessage();
input.addEventListener("keypress", e => { if (e.key === "Enter") this.sendMessage(); });
messageInputEl.addEventListener("keypress", e => { if (e.key === "Enter") this.sendMessage(); });
window.addEventListener("focus", () => {
if (!ws || ws.readyState !== WebSocket.OPEN) {
this.connectWebSocket();
@ -195,7 +195,6 @@ function chatApp() {
currentUserId = a.user_id;
currentSessionId = a.session_id;
this.connectWebSocket();
this.loadSessions();
} catch (e) {
console.error("Failed to initialize auth:", e);
this.updateConnectionStatus("disconnected");
@ -247,7 +246,6 @@ function chatApp() {
switchSession(s) {
currentSessionId = s;
hasReceivedInitialMessage = false;
this.loadSessionHistory(s);
this.connectWebSocket();
if (isVoiceMode) {
this.startVoiceSession();
@ -255,24 +253,6 @@ function chatApp() {
sidebar.classList.remove('open');
},
async loadSessionHistory(s) {
try {
const r = await fetch(`http://localhost:8080/api/sessions/${s}`);
const h = await r.json();
const m = document.getElementById("messages");
m.innerHTML = "";
if (h.length === 0) {
this.updateContextUsage(0);
} else {
h.forEach(([role, content]) => {
this.addMessage(role, content, false);
});
this.updateContextUsage(h.length / 20);
}
} catch (e) {
console.error("Failed to load session history:", e);
}
},
connectWebSocket() {
if (ws) {
@ -526,7 +506,7 @@ function chatApp() {
const b = document.createElement('button');
b.textContent = v.text;
b.className = 'suggestion-button';
b.onclick = () => { this.setContext(v.context); input.value = ''; };
b.onclick = () => { this.setContext(v.context); messageInputEl.value = ''; };
suggestionsContainer.appendChild(b);
});
},
@ -535,7 +515,8 @@ function chatApp() {
try {
const t = event?.target?.textContent || c;
this.addMessage("user", t);
input.value = '';
messageInputEl.value = '';
messageInputEl.value = '';
if (ws && ws.readyState === WebSocket.OPEN) {
pendingContextChange = new Promise(r => {
const h = e => {
@ -566,7 +547,7 @@ function chatApp() {
await pendingContextChange;
pendingContextChange = null;
}
const m = input.value.trim();
const m = messageInputEl.value.trim();
if (!m || !ws || ws.readyState !== WebSocket.OPEN) {
if (!ws || ws.readyState !== WebSocket.OPEN) {
this.showWarning("Conexão não disponível. Tentando reconectar...");
@ -580,8 +561,8 @@ function chatApp() {
this.addMessage("user", m);
const d = { bot_id: currentBotId, user_id: currentUserId, session_id: currentSessionId, channel: "web", content: m, message_type: 1, media_url: null, timestamp: new Date().toISOString() };
ws.send(JSON.stringify(d));
input.value = "";
input.focus();
messageInputEl.value = "";
messageInputEl.focus();
},
async toggleVoiceMode() {

View file

@ -14,7 +14,7 @@
</head>
<body>
<nav x-data="{ current: 'drive' }">
<nav x-data="{ current: 'chat' }">
<div class="logo">⚡ General Bots</div>
<a href="#chat" @click.prevent="current = 'chat'; window.switchSection('chat')"
:class="{ active: current === 'chat' }">💬 Chat</a>

View file

@ -6,9 +6,16 @@ const sections = {
};
const sectionCache = {};
function getBasePath() {
// All static assets (HTML, CSS, JS) are served from the site root.
// Returning a leading slash ensures URLs like "/drive/drive.html" resolve correctly
// with the Actix static file configuration.
return '/';
}
async function loadSectionHTML(path) {
const response = await fetch(path);
if (!response.ok) throw new Error('Failed to load section');
const fullPath = getBasePath() + path;
const response = await fetch(fullPath);
if (!response.ok) throw new Error('Failed to load section: ' + fullPath);
return await response.text();
}
@ -18,7 +25,8 @@ async function switchSection(section) {
try {
const htmlPath = sections[section];
console.log('Loading section:', section, 'from', htmlPath);
const cssPath = htmlPath.replace('.html', '.css');
// Resolve CSS path relative to the base directory.
const cssPath = getBasePath() + htmlPath.replace('.html', '.css');
// Remove any existing section CSS
document.querySelectorAll('link[data-section-css]').forEach(link => link.remove());
@ -82,7 +90,8 @@ async function switchSection(section) {
}
// Then load JS after HTML is inserted (skip if already loaded)
const jsPath = htmlPath.replace('.html', '.js');
// Resolve JS path relative to the base directory.
const jsPath = getBasePath() + htmlPath.replace('.html', '.js');
const existingScript = document.querySelector(`script[src="${jsPath}"]`);
if (!existingScript) {
const script = document.createElement('script');
@ -99,13 +108,32 @@ async function switchSection(section) {
}
// Handle initial load based on URL hash
function getInitialSection() {
// 1⃣ Prefer hash fragment (e.g., #chat)
let section = window.location.hash.substring(1);
// 2⃣ Fallback to pathname segments (e.g., /chat)
if (!section) {
const parts = window.location.pathname.split('/').filter(p => p);
const last = parts[parts.length - 1];
if (['drive', 'tasks', 'mail', 'chat'].includes(last)) {
section = last;
}
}
// 3⃣ As a last resort, inspect the full URL for known sections
if (!section) {
const match = window.location.href.match(/\/(drive|tasks|mail|chat)(?:\.html)?(?:[?#]|$)/i);
if (match) {
section = match[1].toLowerCase();
}
}
// Default to drive if nothing matches
return section || 'drive';
}
window.addEventListener('DOMContentLoaded', () => {
const initialSection = window.location.hash.substring(1) || 'drive';
switchSection(initialSection);
switchSection(getInitialSection());
});
// Handle browser back/forward navigation
window.addEventListener('popstate', () => {
const section = window.location.hash.substring(1) || 'drive';
switchSection(section);
switchSection(getInitialSection());
});