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:
parent
41fd167d50
commit
e38554ea51
4 changed files with 62 additions and 54 deletions
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ function chatApp() {
|
||||||
|
|
||||||
// Core state variables (shared via closure)
|
// Core state variables (shared via closure)
|
||||||
let ws = null,
|
let ws = null,
|
||||||
|
pendingContextChange = null,
|
||||||
currentSessionId = null,
|
currentSessionId = null,
|
||||||
currentUserId = null,
|
currentUserId = null,
|
||||||
currentBotId = "default_bot",
|
currentBotId = "default_bot",
|
||||||
|
|
@ -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() {
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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);
|
|
||||||
});
|
});
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue