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) { pub fn configure_app(cfg: &mut actix_web::web::ServiceConfig) {
let static_path = Path::new("./web/desktop"); let static_path = Path::new("./web/desktop");
@ -50,7 +41,6 @@ pub fn configure_app(cfg: &mut actix_web::web::ServiceConfig) {
.use_etag(true) .use_etag(true)
); );
cfg.service( cfg.service(
Files::new("/chat", static_path.join("chat")) Files::new("/chat", static_path.join("chat"))
.prefer_utf8(true) .prefer_utf8(true)
@ -71,6 +61,15 @@ pub fn configure_app(cfg: &mut actix_web::web::ServiceConfig) {
.use_last_modified(true) .use_last_modified(true)
.use_etag(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,7 +2,8 @@ function chatApp() {
// Core state variables (shared via closure) // Core state variables (shared via closure)
let ws = null, let ws = null,
currentSessionId = null, pendingContextChange = null,
currentSessionId = null,
currentUserId = null, currentUserId = null,
currentBotId = "default_bot", currentBotId = "default_bot",
isStreaming = false, isStreaming = false,
@ -29,7 +30,7 @@ function chatApp() {
const maxReconnectAttempts = 5; const maxReconnectAttempts = 5;
// DOM references (cached for performance) // 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 }); marked.setOptions({ breaks: true, gfm: true });
@ -98,10 +99,11 @@ function chatApp() {
// Lifecycle / event handlers // Lifecycle / event handlers
// ---------------------------------------------------------------------- // ----------------------------------------------------------------------
init() { init() {
document.addEventListener('ready', () => { window.addEventListener('load', () => {
// Assign DOM elements after the document is ready // Assign DOM elements after the document is ready
messagesDiv = document.getElementById("messages"); messagesDiv = document.getElementById("messages");
input = document.getElementById("messageInput");
messageInputEl = document.getElementById("messageInput");
sendBtn = document.getElementById("sendBtn"); sendBtn = document.getElementById("sendBtn");
voiceBtn = document.getElementById("voiceBtn"); voiceBtn = document.getElementById("voiceBtn");
connectionStatus = document.getElementById("connectionStatus"); connectionStatus = document.getElementById("connectionStatus");
@ -125,13 +127,11 @@ function chatApp() {
this.applyTheme(); this.applyTheme();
} }
}); });
input.focus(); messageInputEl.focus();
// UI event listeners // UI event listeners
document.addEventListener('click', (e) => { document.addEventListener('click', (e) => {
if (sidebar.classList.contains('open') && !sidebar.contains(e.target) && !floatLogo.contains(e.target)) {
sidebar.classList.remove('open');
}
}); });
messagesDiv.addEventListener('scroll', () => { messagesDiv.addEventListener('scroll', () => {
@ -150,7 +150,7 @@ function chatApp() {
}); });
sendBtn.onclick = () => this.sendMessage(); 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", () => { window.addEventListener("focus", () => {
if (!ws || ws.readyState !== WebSocket.OPEN) { if (!ws || ws.readyState !== WebSocket.OPEN) {
this.connectWebSocket(); this.connectWebSocket();
@ -195,7 +195,6 @@ function chatApp() {
currentUserId = a.user_id; currentUserId = a.user_id;
currentSessionId = a.session_id; currentSessionId = a.session_id;
this.connectWebSocket(); this.connectWebSocket();
this.loadSessions();
} catch (e) { } catch (e) {
console.error("Failed to initialize auth:", e); console.error("Failed to initialize auth:", e);
this.updateConnectionStatus("disconnected"); this.updateConnectionStatus("disconnected");
@ -247,7 +246,6 @@ function chatApp() {
switchSession(s) { switchSession(s) {
currentSessionId = s; currentSessionId = s;
hasReceivedInitialMessage = false; hasReceivedInitialMessage = false;
this.loadSessionHistory(s);
this.connectWebSocket(); this.connectWebSocket();
if (isVoiceMode) { if (isVoiceMode) {
this.startVoiceSession(); this.startVoiceSession();
@ -255,24 +253,6 @@ function chatApp() {
sidebar.classList.remove('open'); 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() { connectWebSocket() {
if (ws) { if (ws) {
@ -526,7 +506,7 @@ function chatApp() {
const b = document.createElement('button'); const b = document.createElement('button');
b.textContent = v.text; b.textContent = v.text;
b.className = 'suggestion-button'; b.className = 'suggestion-button';
b.onclick = () => { this.setContext(v.context); input.value = ''; }; b.onclick = () => { this.setContext(v.context); messageInputEl.value = ''; };
suggestionsContainer.appendChild(b); suggestionsContainer.appendChild(b);
}); });
}, },
@ -535,7 +515,8 @@ function chatApp() {
try { try {
const t = event?.target?.textContent || c; const t = event?.target?.textContent || c;
this.addMessage("user", t); this.addMessage("user", t);
input.value = ''; messageInputEl.value = '';
messageInputEl.value = '';
if (ws && ws.readyState === WebSocket.OPEN) { if (ws && ws.readyState === WebSocket.OPEN) {
pendingContextChange = new Promise(r => { pendingContextChange = new Promise(r => {
const h = e => { const h = e => {
@ -566,7 +547,7 @@ function chatApp() {
await pendingContextChange; await pendingContextChange;
pendingContextChange = null; pendingContextChange = null;
} }
const m = input.value.trim(); const m = messageInputEl.value.trim();
if (!m || !ws || ws.readyState !== WebSocket.OPEN) { if (!m || !ws || ws.readyState !== WebSocket.OPEN) {
if (!ws || ws.readyState !== WebSocket.OPEN) { if (!ws || ws.readyState !== WebSocket.OPEN) {
this.showWarning("Conexão não disponível. Tentando reconectar..."); this.showWarning("Conexão não disponível. Tentando reconectar...");
@ -580,8 +561,8 @@ function chatApp() {
this.addMessage("user", m); 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() }; 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)); ws.send(JSON.stringify(d));
input.value = ""; messageInputEl.value = "";
input.focus(); messageInputEl.focus();
}, },
async toggleVoiceMode() { async toggleVoiceMode() {

View file

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

View file

@ -6,9 +6,16 @@ const sections = {
}; };
const sectionCache = {}; 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) { async function loadSectionHTML(path) {
const response = await fetch(path); const fullPath = getBasePath() + path;
if (!response.ok) throw new Error('Failed to load section'); const response = await fetch(fullPath);
if (!response.ok) throw new Error('Failed to load section: ' + fullPath);
return await response.text(); return await response.text();
} }
@ -18,7 +25,8 @@ async function switchSection(section) {
try { try {
const htmlPath = sections[section]; const htmlPath = sections[section];
console.log('Loading section:', section, 'from', htmlPath); 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 // Remove any existing section CSS
document.querySelectorAll('link[data-section-css]').forEach(link => link.remove()); 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) // 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}"]`); const existingScript = document.querySelector(`script[src="${jsPath}"]`);
if (!existingScript) { if (!existingScript) {
const script = document.createElement('script'); const script = document.createElement('script');
@ -99,13 +108,32 @@ async function switchSection(section) {
} }
// Handle initial load based on URL hash // 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', () => { window.addEventListener('DOMContentLoaded', () => {
const initialSection = window.location.hash.substring(1) || 'drive'; switchSection(getInitialSection());
switchSection(initialSection);
}); });
// Handle browser back/forward navigation // Handle browser back/forward navigation
window.addEventListener('popstate', () => { window.addEventListener('popstate', () => {
const section = window.location.hash.substring(1) || 'drive'; switchSection(getInitialSection());
switchSection(section);
}); });